Test in the sandbox
Test the Coris API with mocked business data, fixed test scenarios and no production impact.
Test your integration at https://sandbox-api.coris.ai/api/v0_2 with mocked data and no production impact. Most responses are built from what you send, so fictional businesses work.
- StartYour request
- CheckWhich step?
- Ratings,
TIN, OFACMocked with test scenarios - CorShield,
other profile dataMocked, no scenarios - Business
SearchRuns against sandbox records - Accounts,
webhooks, tagsReal sandbox objects
Test the scenarios
- Set your key.
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`);
}- Send a test value. This request returns a poor business rating.
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.",
"business_description": "bad_business great_website",
"address": { "address_type": "Registration", "city": "San Jose", "state": "CA", "country": "US" },
"data_types": ["business_search", "establishment_ratings"]
}' | jq -r '.request_id')
poll /merchant_profiler/ "$REQUEST_ID" | jq '.establishment_ratings'response = requests.post(
f"{BASE_URL}/merchant_profiler/",
headers=HEADERS,
json={
"business_name": "Acme Plumbing Co.",
"business_description": "bad_business great_website",
"address": {
"address_type": "Registration",
"city": "San Jose",
"state": "CA",
"country": "US",
},
"data_types": ["business_search", "establishment_ratings"],
},
)
response.raise_for_status()
print(poll("/merchant_profiler/", response.json()["request_id"])["establishment_ratings"])const created = await fetch(`${BASE_URL}/merchant_profiler/`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
business_name: "Acme Plumbing Co.",
business_description: "bad_business great_website",
address: { address_type: "Registration", city: "San Jose", state: "CA", country: "US" },
data_types: ["business_search", "establishment_ratings"],
}),
});
const { request_id: requestId } = await created.json();
console.log((await poll("/merchant_profiler/", requestId)).establishment_ratings);Try POST /merchant_profiler/ in the API explorer.
- Change the input to reach another scenario.
| Send this | On this endpoint | You get |
|---|---|---|
great_business, good_business, ok_business, bad_business or terrible_business in business_description | Create a Merchant Profiler request | biz_rating is 1, 2, 3, 4 or 5, where 1 is best. With no test value it is 3. |
The same words ending in _website in business_description | Create a Merchant Profiler request | website_rating, on the same scale. |
AML_OFAC in business_name when you create the record | Get OFAC and AML screening | number_of_hits is 1. Any other name returns 0. |
ein 000000000 | Verify a business | tin_match is false. |
ein with any other 9 digits, except the value below | Verify a business | tin_match is true. |
ein 123456789 | Verify a business | The request stays at 206, so tin_match is absent and the polling helper times out. Use it only to test timeouts. |
No test value sets an inactive registration. status in legal_entity_registrations is random, so you cannot force that decline route. Test it with a fixed sample in your own unit tests.
What differs from production
Treat every returned business as test data. The sandbox differs from production in these ways.
| Area | In the sandbox | In production |
|---|---|---|
| Merchant Profiler | Matches sandbox records, or creates a business from your request. A new record has no website, phone or address on establishment. Your address is under legal_entity.addresses, and legal_entity.status is UNKNOWN or null. | Real checks. Fields appear when Coris has them. |
| Profile sections | Mocked. Many values are random or fixed, so they can disagree with each other and with your request. | Collected for the business. |
Social media timestamp | An ISO 8601 string. | Epoch seconds. |
| Business Search | Runs real matching against sandbox records. A fictional name can return no results, or match a record from an earlier test. | Runs against production records. |
| OFAC and TIN checks | Mocked. Use the test values above. | Real checks. |
| CorShield, standalone | No coris_risk_bucket or portal_link. adverse_reasons holds one sample reason, and findings and flags are empty. | Both fields are present. Reasons and flags describe the business. |
| CorShield, in Merchant Profiler | application_data fills gaps with invented values, such as tin, ip_address, a numeric requested_spend and an applicant. Reasons are random sample text. Findings and flags hold placeholders such as detailed red flag 2. | application_data holds what you sent. Reasons and flags describe the business. |
| PDF reports | pdf_report_url can be null, with is_complete false. | A report URL, or null if the build fails. |
| Create or update an account | mcc_code and mcc_description are not stored. boarded_iso is not stored on create, so set it with Update an account. | All three are stored. |
Transaction status | Disputed returns Captured, with disputed set to true. | Returns Disputed. |
Person date_of_birth | Epoch seconds on every route. | YYYY-MM-DD, except the list route, which returns epoch seconds. |
Person verification_status | Can stay pending. | Changes when a check runs. |
Mock values can fall outside the values listed in the reference, so do not fail on an unknown value. Examples are licensing_status ACTIVE, phone_type NonFixedVOIP and account address.country USA.
Test CorShield
CorShield has no test scenarios, so you cannot choose a score. To cover each score band, use fixed scores in your own unit tests.
Run a CorShield evaluation derives its mock score from your request, so the same request usually repeats the score. The corshield section of Merchant Profiler picks a random score on every run. Use the score from the run you are testing.
A standalone application_data holds what you sent. If reference_id or account_id matches one of your accounts, Coris fills gaps from that account. Production does this too.
Test alerts and processor data
The public API has no test trigger for alerts. Test your alert handling in two parts:
- Register an endpoint for
account.tag.created, and apply a new tag to an account. The delivery proves your URL and signature check work. A tag already on the account sendsaccount.tag.updatedinstead. See Webhooks. - Sign a sample
alert.createdbody with your own secret, and post it to your handler. Monitor your portfolio shows the fields.
The sandbox also has no fixed Stripe test IDs and cannot simulate a processor event. Connect a Stripe test key in the sandbox portal, and its test events then reach Coris. See Connect Stripe and Adyen.
To find a stored charge, call GET /accounts/?source=stripe, then GET /accounts/{account_id}/transactions/ for an account it returns.
What to do next
| You want to test | Use |
|---|---|
| The happy path | The Quickstart. |
| Reviews and holds | The scenarios table above. |
| Declines | A fixed INACTIVE sample in your own unit tests. |
| Webhook and alert handling | The steps under Test alerts and processor data. |
| Going live | Swap the host and the key. Your calls stay the same, and results then come from real checks. |
FAQ
Does the sandbox cost anything?
Sandbox requests are free.
Can I use real merchant data in the sandbox?
Use fictional businesses and domains ending in .example, never real merchant data.
Does sandbox data reach production?
No. Each environment has its own keys and hosts.
Updated about 23 hours ago