Verify a business at onboarding
Run KYB at onboarding with Merchant Profiler registration data, OFAC and AML screening, and a CorShield risk score.
Check that a business is real and that the applicant represents it, with registration data, OFAC and AML screening, and an optional CorShield risk score.
- You → Coris1POST
/ merchant_profiler/ - Coris → You2201
request_id - You → Coris3GET
/ merchant_profiler/ ? request_id= - Coris → You4200
establishment, legal_entity, legal_entity_registrations - You → Coris5GET
/ legal_entity/ {legal_entity_id}/ ofac_aml/ - Coris → You6200
number_of_hits - You → Coris7POST
/ corshield/ fraud_model/optional - Coris → You8201
request_id - You → Coris9GET
/ corshield/ fraud_model/ ? request_id= - Coris → You10200
third_ party_ models. fraud_ score
Step 1: Set up your key
Set your key and base URL once.
export CORIS_BASE_URL="https://sandbox-api.coris.ai/api/v0_2"
export CORIS_API_KEY="YOUR_API_KEY"import requests
BASE_URL = "https://sandbox-api.coris.ai/api/v0_2"
HEADERS = {"Authorization": "Token YOUR_API_KEY"}const BASE_URL = "https://sandbox-api.coris.ai/api/v0_2";
const HEADERS = {
Authorization: "Token YOUR_API_KEY",
"Content-Type": "application/json",
};The samples below wait with the polling helper. Expand it, and run it too.
Polling helper
# 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
# 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")// 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 2: Get registration data
Request the business records, and use data_types to limit the job to the sections you need.
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" },
"data_types": ["business_search", "legal_entity_registrations", "personnel"]
}' | jq -r '.request_id')
PROFILE=$(poll /merchant_profiler/ "$REQUEST_ID")
LEGAL_ENTITY_ID=$(printf '%s' "$PROFILE" | jq -r '.legal_entity.id')
printf '%s' "$PROFILE" | jq '{legal_entity, legal_entity_registrations}'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",
},
"data_types": ["business_search", "legal_entity_registrations", "personnel"],
},
)
response.raise_for_status()
profile = poll("/merchant_profiler/", response.json()["request_id"])
legal_entity_id = profile["legal_entity"]["id"]
print(profile["legal_entity"], profile.get("legal_entity_registrations"))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" },
data_types: ["business_search", "legal_entity_registrations", "personnel"],
}),
});
if (!created.ok) throw new Error(await created.text());
const profile = await poll("/merchant_profiler/", (await created.json()).request_id);
const legalEntityId = profile.legal_entity.id;
console.log(profile.legal_entity, profile.legal_entity_registrations);Try POST /merchant_profiler/ in the API explorer.
Step 3: Screen for OFAC and AML
Check the legal entity for OFAC and AML hits, using legal_entity.id from step 2.
curl -s "$CORIS_BASE_URL/legal_entity/$LEGAL_ENTITY_ID/ofac_aml/" \
-H "Authorization: Token $CORIS_API_KEY" | jqresponse = requests.get(f"{BASE_URL}/legal_entity/{legal_entity_id}/ofac_aml/", headers=HEADERS)
print(response.json())const screening = await fetch(`${BASE_URL}/legal_entity/${legalEntityId}/ofac_aml/`, {
headers: HEADERS,
});
console.log(await screening.json());Try GET /legal_entity/{legal_entity_id}/ofac_aml/ in the API explorer.
Step 4 (optional): Add a CorShield risk score
Run CorShield to get a 0–100 risk score for your review. Set first_party to false unless Coris has enabled that model for you. The default is true, and a disabled model returns 400.
To get the profile and the score in one call, skip this step and change step 2. Add business_fraud_check to data_types, set first_party to false, and send the same email, website and principal fields. The score arrives under corshield.
REQUEST_ID=$(curl -s -X POST "$CORIS_BASE_URL/corshield/fraud_model/" \
-H "Authorization: Token $CORIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"first_party": false,
"third_party": true,
"reference_id": "acme-plumbing-001",
"business_name": "Acme Plumbing Co.",
"email": "[email protected]",
"phone_number": "408-555-0142",
"website": "https://www.acmeplumbing.example",
"address": { "city": "San Jose", "state": "CA", "country": "US" },
"principal": { "first_name": "Jordan", "last_name": "Sample" }
}' | jq -r '.request_id')
poll /corshield/fraud_model/ "$REQUEST_ID" | jq '{reference_id, third_party_models}'response = requests.post(
f"{BASE_URL}/corshield/fraud_model/",
headers=HEADERS,
json={
"first_party": False,
"third_party": True,
"reference_id": "acme-plumbing-001",
"business_name": "Acme Plumbing Co.",
"email": "[email protected]",
"phone_number": "408-555-0142",
"website": "https://www.acmeplumbing.example",
"address": {"city": "San Jose", "state": "CA", "country": "US"},
"principal": {"first_name": "Jordan", "last_name": "Sample"},
},
)
response.raise_for_status()
result = poll("/corshield/fraud_model/", response.json()["request_id"])
print(result["third_party_models"])const shieldStart = await fetch(`${BASE_URL}/corshield/fraud_model/`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
first_party: false,
third_party: true,
reference_id: "acme-plumbing-001",
business_name: "Acme Plumbing Co.",
email: "[email protected]",
phone_number: "408-555-0142",
website: "https://www.acmeplumbing.example",
address: { city: "San Jose", state: "CA", country: "US" },
principal: { first_name: "Jordan", last_name: "Sample" },
}),
});
if (!shieldStart.ok) throw new Error(await shieldStart.text());
const result = await poll("/corshield/fraud_model/", (await shieldStart.json()).request_id);
console.log(result.third_party_models);Try POST /corshield/fraud_model/ in the API explorer.
Read the response
Route each case on fraud_score, which sits under third_party_models with adverse_reasons.
{
"reference_id": "acme-plumbing-001",
"third_party_models": {
"model_name": "CorShield Third Party Fraud Model",
"fraud_score": 42,
"adverse_reasons": []
}
}| Field | Meaning | What to do |
|---|---|---|
fraud_score | A 0–100 risk score. Higher means more risk. It is not a probability. | Route the case with it. See How to read Coris scores. |
adverse_reasons | Plain-language reasons for the score. | Show them to your reviewer. |
The OFAC and AML result from step 3 holds status, number_of_hits and, when there are matches, hits.
Let Coris run your onboarding checks
To run these checks through your own rules, create an account instead.
Check a TIN
Use Verify a business only when you need an IRS TIN match, and read match_details.tin_match from the result. Send verification_types with TIN_VERIFICATION, plus legal_name, ein and address. The address needs country, plus postal_code, zip_code, or city with state.
What to do next
Route each result with this example policy, and tune it to your risk appetite. Check the rows in order, stop at the first match, and skip any check you did not run.
| Result | Action |
|---|---|
| Any OFAC or AML hit | Hold, and escalate to compliance. |
| Inactive registration | Decline, or ask the merchant to cure it. |
| Risk score above 75 | Ask for documents before you decide. |
| Adverse media or litigation found | Send to a senior reviewer. |
Risk score from 25 to 75, an unknown registration status, or a biz_rating or website_rating of 4 or 5 | Send to manual review. |
| Industry differs from the declared MCC | Review, and correct the MCC. |
| Active registration, no hits, risk score below 25 | Approve at standard limits. |
An active registration means legal_entity.status is ACTIVE. If it is UNKNOWN, read status in legal_entity_registrations, and treat a value such as ACTIVE or GOOD STANDING as active. Ratings run from 1, the best, to 5.
This example has no score-based decline. Add one if your policy calls for it.
FAQ
What if the business has no registration?
Sole proprietors may have none, so Coris creates a record and status can be UNKNOWN. Review these by hand.
Do I have to call these one at a time?
No. CorShield can run alongside the profile. OFAC and AML screening needs legal_entity.id, so it follows step 2.
Why is coris_risk_bucket missing from my result?
coris_risk_bucket missing from my result?The sandbox leaves coris_risk_bucket and portal_link out of a standalone CorShield result. Route on fraud_score, or use the one-call route in step 4.
Next: Underwrite a merchant
Updated about 23 hours ago