Handle async requests

Poll with request_id or receive a signed callback for Merchant Profiler, CorShield, verification and other async Coris endpoints.

Each POST returns 201 with a request_id, and the job continues in the background. Get the result by polling with that ID, or by receiving a callback.

YouCoris
  1. You → Coris
    1POST /merchant_profiler/
  2. Coris → You
    2201 request_id
  3. Loop: Until 200
    1. You → Coris
      3GET /merchant_profiler/?request_id=...
    2. Coris → You
      4425 no data yet
    3. Coris → You
      5206 partial data
  4. Coris → You
    6200 complete
  5. Coris → You
    7POST callback_urlif you sent one

Add the polling helper

Every guide that starts a job uses this poll helper. It backs off from 5 to 30 seconds and stops after 30 attempts.

export CORIS_BASE_URL="https://sandbox-api.coris.ai/api/v0_2"
export CORIS_API_KEY="YOUR_API_KEY"

# Polls an async endpoint and prints the body once it returns 200.
# 206, 425, 429 and 500 all mean wait and poll again. A 500 re-queues the job.
poll() {
  delay=5
  for _ in $(seq 1 30); do
    http_status=$(curl -s -o /tmp/coris_body -w '%{http_code}' \
      -H "Authorization: Token $CORIS_API_KEY" \
      "$CORIS_BASE_URL$1?request_id=$2")
    if [ "$http_status" = "200" ]; then cat /tmp/coris_body; return 0; fi
    case "$http_status" in
      206|425|429|500) ;;
      *) echo "HTTP $http_status: $(cat /tmp/coris_body)" >&2; return 1 ;;
    esac
    sleep "$delay"
    delay=$((delay * 3 / 2)); [ "$delay" -gt 30 ] && delay=30
  done
  echo "Timed out" >&2; return 1
}
import time
import requests

BASE_URL = "https://sandbox-api.coris.ai/api/v0_2"
HEADERS = {"Authorization": "Token YOUR_API_KEY"}


# 206, 425, 429 and 500 all mean wait and poll again. A 500 re-queues the job.
RETRYABLE = (206, 425, 429, 500)


def poll(path, request_id, attempts=30):
    """Polls an async endpoint and returns the JSON body once it returns 200."""
    delay = 5
    for _ in range(attempts):
        response = requests.get(
            f"{BASE_URL}{path}", params={"request_id": request_id}, headers=HEADERS
        )
        if response.status_code == 200:
            return response.json()
        if response.status_code not in RETRYABLE:
            response.raise_for_status()
        time.sleep(delay)
        delay = min(delay * 1.5, 30)
    raise TimeoutError(f"{path} did not finish")
const BASE_URL = "https://sandbox-api.coris.ai/api/v0_2";
const HEADERS = {
  Authorization: "Token YOUR_API_KEY",
  "Content-Type": "application/json",
};

// 206, 425, 429 and 500 all mean wait and poll again. A 500 re-queues the job.
const RETRYABLE = [206, 425, 429, 500];

// Polls an async endpoint and returns the JSON body once it returns 200.
async function poll(path, requestId, attempts = 30) {
  let delay = 5000;
  for (let i = 0; i < attempts; i++) {
    const url = `${BASE_URL}${path}?request_id=${requestId}`;
    const response = await fetch(url, { headers: HEADERS });
    if (response.status === 200) return response.json();
    if (!RETRYABLE.includes(response.status)) {
      throw new Error(`HTTP ${response.status}: ${await response.text()}`);
    }
    await new Promise((resolve) => setTimeout(resolve, delay));
    delay = Math.min(delay * 1.5, 30000);
  }
  throw new Error(`${path} did not finish`);
}

Step 1: Start a job

Start a job, and keep the request_id it returns.

REQUEST_ID=$(curl -s -X POST "$CORIS_BASE_URL/merchant_profiler/" \
  -H "Authorization: Token $CORIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "business_name": "Acme Plumbing Co.",
    "address": {
      "address_type": "Registration",
      "city": "San Jose",
      "state": "CA",
      "country": "US"
    },
    "phone_number": "408-555-0142",
    "website": "https://www.acmeplumbing.example"
  }' | jq -r '.request_id')

echo "$REQUEST_ID"
response = requests.post(
    f"{BASE_URL}/merchant_profiler/",
    headers=HEADERS,
    json={
        "business_name": "Acme Plumbing Co.",
        "address": {
            "address_type": "Registration",
            "city": "San Jose",
            "state": "CA",
            "country": "US",
        },
        "phone_number": "408-555-0142",
        "website": "https://www.acmeplumbing.example",
    },
)
response.raise_for_status()
request_id = response.json()["request_id"]
print(request_id)
const created = await fetch(`${BASE_URL}/merchant_profiler/`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    business_name: "Acme Plumbing Co.",
    address: {
      address_type: "Registration",
      city: "San Jose",
      state: "CA",
      country: "US",
    },
    phone_number: "408-555-0142",
    website: "https://www.acmeplumbing.example",
  }),
});
if (!created.ok) throw new Error(await created.text());
const { request_id: requestId } = await created.json();
console.log(requestId);

Try POST /merchant_profiler/ in the API explorer.

Save request_id. You use it to poll, and to match X-Request-ID on callbacks.

These endpoints follow the same pattern: Create a Merchant Profiler request, Search businesses, Verify a business, Run a CorShield evaluation and Create an account. So do establishment and account data POST routes.

The exception is Create an account with establishment_id, which returns the account in its 201.

Step 2: Poll with backoff

Poll with the polling helper until the job returns 200.

PROFILE=$(poll /merchant_profiler/ "$REQUEST_ID")
ESTABLISHMENT_ID=$(printf '%s' "$PROFILE" | jq -r '.establishment.id')
printf '%s' "$PROFILE" | jq '{establishment, legal_entity, portal_link}'
profile = poll("/merchant_profiler/", request_id)
establishment_id = profile["establishment"]["id"]
print(profile["establishment"], profile["legal_entity"], profile.get("portal_link"))
const profile = await poll("/merchant_profiler/", requestId);
const establishmentId = profile.establishment.id;
console.log(profile.establishment, profile.legal_entity, profile.portal_link);
StatusMeaningWhat to do
425No data exists yet.Wait and poll again.
206Partial data is ready.Use it if you can, and keep polling.
200The job is complete.Stop polling.
404No request has that ID for your workspace or endpoint.Check the ID and the path.
429You passed a rate limit.Wait, then poll again.
500The job failed. On most async endpoints, this poll re-queues it.Keep polling up to the helper's attempt cap. Then contact support.

Keep polling a stalled job, because a poll restarts a job idle for 5 minutes. A malformed request_id returns 500 every time, so it ends at the same cap.

Step 3 (optional): Receive a callback instead

Callbacks suit background work at volume. Send callback_url in the POST body, and use a public HTTPS URL.

Coris posts the same JSON you would get from the final GET. Merchant Profiler sends its callback only when all sections finish.

curl -s -X POST "$CORIS_BASE_URL/merchant_profiler/" \
  -H "Authorization: Token $CORIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "business_name": "Acme Plumbing Co.",
    "address": { "address_type": "Registration", "city": "San Jose", "state": "CA", "country": "US" },
    "callback_url": "https://hooks.acmeplatform.example/coris/callback"
  }'
response = requests.post(
    f"{BASE_URL}/merchant_profiler/",
    headers=HEADERS,
    json={
        "business_name": "Acme Plumbing Co.",
        "address": {
            "address_type": "Registration",
            "city": "San Jose",
            "state": "CA",
            "country": "US",
        },
        "callback_url": "https://hooks.acmeplatform.example/coris/callback",
    },
)
print(response.status_code, response.json())
const withCallback = await fetch(`${BASE_URL}/merchant_profiler/`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    business_name: "Acme Plumbing Co.",
    address: { address_type: "Registration", city: "San Jose", state: "CA", country: "US" },
    callback_url: "https://hooks.acmeplatform.example/coris/callback",
  }),
});
console.log(withCallback.status, await withCallback.json());

Step 4: Verify the callback

HeaderMeaning
X-Request-IDThe request_id from your POST.
X-CORIS-SIGNATUREHex HMAC-SHA256 of the raw body, using your callback secret.

Coris signs callbacks only when your workspace has a callback secret. Request one.

# BODY holds the raw request body. SIGNATURE holds the header value.
EXPECTED=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$CALLBACK_SECRET" -hex | awk '{print $NF}')
[ "$EXPECTED" = "$SIGNATURE" ] && echo "valid" || echo "invalid"
import hashlib
import hmac


def verify_callback(raw_body: bytes, signature: str, callback_secret: str) -> bool:
    expected = hmac.new(callback_secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
import crypto from "node:crypto";

function verifyCallback(rawBody, signature, callbackSecret) {
  const expected = crypto.createHmac("sha256", callbackSecret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Return any 2xx to confirm delivery. Coris makes up to 32 attempts, up to 30 minutes apart.

What to do next

SituationChoice
A person is waitingPoll, and show 206 data as it arrives.
You need every sectionWait for 200 or the callback.
A section holds an error key on 200That section failed, and the rest is valid. Adverse media returns an empty list instead.

FAQ

Can I use polling and callbacks together?

Yes. Polling also covers a missed callback.

How long is a request_id valid?

Results have no set expiry. Store what you need instead of relying on a later GET.

Should I send the same POST again if polling is slow?

A new POST starts a new job, so keep polling the original request_id.

Next: Receive webhooks


Did this page help you?