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.

  1. StartYour request
  2. 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

  1. 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`);
}
  1. 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.

  1. Change the input to reach another scenario.
Send thisOn this endpointYou get
great_business, good_business, ok_business, bad_business or terrible_business in business_descriptionCreate a Merchant Profiler requestbiz_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_descriptionCreate a Merchant Profiler requestwebsite_rating, on the same scale.
AML_OFAC in business_name when you create the recordGet OFAC and AML screeningnumber_of_hits is 1. Any other name returns 0.
ein 000000000Verify a businesstin_match is false.
ein with any other 9 digits, except the value belowVerify a businesstin_match is true.
ein 123456789Verify a businessThe 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.

AreaIn the sandboxIn production
Merchant ProfilerMatches 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 sectionsMocked. Many values are random or fixed, so they can disagree with each other and with your request.Collected for the business.
Social media timestampAn ISO 8601 string.Epoch seconds.
Business SearchRuns 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 checksMocked. Use the test values above.Real checks.
CorShield, standaloneNo 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 Profilerapplication_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 reportspdf_report_url can be null, with is_complete false.A report URL, or null if the build fails.
Create or update an accountmcc_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 statusDisputed returns Captured, with disputed set to true.Returns Disputed.
Person date_of_birthEpoch seconds on every route.YYYY-MM-DD, except the list route, which returns epoch seconds.
Person verification_statusCan 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 sends account.tag.updated instead. See Webhooks.
  • Sign a sample alert.created body 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 testUse
The happy pathThe Quickstart.
Reviews and holdsThe scenarios table above.
DeclinesA fixed INACTIVE sample in your own unit tests.
Webhook and alert handlingThe steps under Test alerts and processor data.
Going liveSwap 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.

Next: Integrate business and KYB data


Did this page help you?