2. Onboard a customer
Terms, the customer with its documents, the explicit submit, and the endorsement that opens everything else.
Prerequisites
- A sandbox key exported as
$KEY, and$BASEset tohttps://sandbox.rasto.co. See Get set up. - Any three image files to stand in for a passport, a selfie, and a proof of address. Sandbox does not inspect them, but the intake requires them to be present.
The shape of onboarding
Four calls, in this order. None of them is optional, and the order is not negotiable.
Accept the terms of service
A customer cannot exist without an accepted agreement. Mint a hosted link
and send your user to its url:
curl -s $BASE/api/tos_links \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"redirect_url": "https://yourapp.example/tos-done"}' | jq .
{
"id": "tos_033y0AbCdEfGhIjKlMnOpQ",
"object": "tos_link",
"url": "https://sandbox.rasto.co/tos/accept?session_token=...",
"tos_version": null,
"status": "pending",
"signed_tos_id": null,
"customer_id": null,
"created_at": "2026-08-26T09:14:02+00:00"
}
Acceptance mints a signed_tos_id (sig_...). For this walkthrough you
can skip the hosted page entirely and attest inline in the next step
instead, which is what you would do if you present our terms in your own
UI. See Terms of Service for both paths.
Create the customer with its documents
The customer object is the verification intake. Identity fields, base64 documents, and the endorsements you want all ride one call.
curl -s $BASE/api/customers \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{
"kind": "individual",
"first_name": "Ana",
"last_name": "Silva",
"email": "ana@example.com",
"phone": "+14155550123",
"date_of_birth": "1993-04-12",
"nationality": "US",
"residence_country": "US",
"address": {
"street_line_1": "300 Post St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94108",
"country": "US"
},
"economic_profile": {"tin": "078-05-1120", "tax_residence_country": "US"},
"endorsements": ["endo_usd"],
"tos_acceptance": {"ip_address": "203.0.113.7"},
"identifying_information": [
{"kind": "passport", "issuing_country": "USA", "image_front": "<base64>"}
],
"documents": [
{"purpose": "selfie", "file": "<base64>"},
{"purpose": "proof_of_address", "file": "<base64>"}
],
"partner_reference_id": "user-8213"
}' | jq .
Two things in that body are worth understanding before you send it.
endorsements: ["endo_usd"] is what enrolls the customer. Enrollment
is explicit: without it, the customer never reaches USD verification at
all, and virtual-account creation later fails with
400 endorsement_required.
Requesting endo_usd requires its profile fields in the same request:
email, phone, economic_profile.tin, and
economic_profile.tax_residence_country. Missing ones return
422 endorsement_profile_required naming them, in sandbox as well as live,
so integrations learn the contract before go-live.
The response embeds the endorsement with its five requirement buckets:
"endorsements": [
{
"name": "endo_usd",
"status": "incomplete",
"requirements": {
"complete": ["terms_of_service"],
"uploaded": ["identity", "selfie", "proof_of_address"],
"pending": [],
"missing": [],
"issues": []
}
}
]
uploaded means we hold the documents but nothing is reviewing them yet,
because you have not submitted. Do not re-send them.
Set partner_reference_id to your own user id. It is unique per
environment, filterable on list endpoints, and the cleanest way to join
Rasto records to yours.
Submit for verification
This is the only call that submits the customer for review. Documents sent in the previous step are archived and staged, and nothing happens to them until now.
curl -s $BASE/api/customers/cus_.../verifications/submit \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{}' | jq .
{
"id": "ver_033y1FhTOmoW9W2jAuSvQx",
"object": "verification",
"customer_id": "cus_033y1FhTOmoW9W2jAuSvQx",
"intake_method": "direct",
"purpose": "full",
"requested_info": null,
"url": null,
"status": "submitted",
"rejection_reasons": null,
"created_at": "2026-08-26T09:15:32+00:00"
}
If anything required is still outstanding, this returns
422 verification_incomplete and names it, rather than failing opaquely
further down. Individuals owe an identity document, a selfie, and a proof
of address. Businesses owe a registration document.
Supply what is missing with a PATCH /api/customers/{id}, then submit
again. Submitting repeatedly is safe: each call opens a new round on the
same applicant, never a second identity.
Approve the decision
In production the decision arrives on a webhook. In sandbox you make it:
curl -s $BASE/api/sandbox/simulate/verification_decision \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"customer_id": "cus_...", "decision": "approved"}' | jq .
The requested endorsement approves alongside it. Read it back:
curl -s $BASE/api/customers/cus_... -H "Api-Key: $KEY" | jq '.endorsements'
[
{
"name": "endo_usd",
"status": "approved",
"requirements": {
"complete": ["terms_of_service", "identity", "selfie", "proof_of_address", "aml"],
"uploaded": [], "pending": [], "missing": [], "issues": []
}
}
]
That approved is the gate for everything that follows.
Rehearse the unhappy path too
Before you move on, run the same customer through
"decision": "request_for_information". The endorsement moves to issues,
the asks appear in the buckets as bare codes, and the fix is a PATCH
followed by another submit. It is the branch your production UI will spend
most of its time in. See
Verification needs more information.