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.
- You → Coris1POST
/ merchant_profiler/ - Coris → You2201
request_id - Loop: Until 200
- You → Coris3GET
/ merchant_profiler/ ? request_id=... - Coris → You4425
no data yet - Coris → You5206
partial data
- You → Coris
- Coris → You6200
complete - Coris → You7POST
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);| Status | Meaning | What to do |
|---|---|---|
425 | No data exists yet. | Wait and poll again. |
206 | Partial data is ready. | Use it if you can, and keep polling. |
200 | The job is complete. | Stop polling. |
404 | No request has that ID for your workspace or endpoint. | Check the ID and the path. |
429 | You passed a rate limit. | Wait, then poll again. |
500 | The 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
| Header | Meaning |
|---|---|
X-Request-ID | The request_id from your POST. |
X-CORIS-SIGNATURE | Hex 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
| Situation | Choice |
|---|---|
| A person is waiting | Poll, and show 206 data as it arrives. |
| You need every section | Wait for 200 or the callback. |
A section holds an error key on 200 | That 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?
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
Updated about 23 hours ago