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 reads every document as the same sample person, but the intake requires the passport and the selfie to be present. The walkthrough's customer lives in the US, so the proof of address is optional for them; we read it when you send one.
Onboarding is four steps, and each one depends on the one before it.
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/api/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_...) that you pass when you
create the customer. For this walkthrough, skip the hosted page and attest
inline in the next step instead, which is what you do when you present our
terms in your own UI. For both paths, see
Terms of Service.
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"
},
"tax_id": "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 .
endorsements: ["endo_usd"] enrolls the customer. Enrollment is
explicit: without it, the customer never reaches USD verification, and
virtual account creation later fails with 400 endorsement_required.
Requesting endo_usd requires its contact fields, already on the customer
or in the same request:
email and phone. Missing ones return
422 endorsement_profile_required naming them, in sandbox as well as live.
tax_id and tax_residence_country are not needed to request it, but the
live review needs them, so send them here. In live, until they arrive, the
endorsement lists them in missing and the submit returns
422 verification_incomplete.
For an individual, the name, date of birth, nationality, and address are
optional. At submit we read them from the identity document and proof of
address and fill only the fields you left empty. A value you send is never
overwritten. The identity document's kind and issuing_country are
optional too: we read both from the image, and ask for them only when they
cannot be read.
A business onboards the same way: send kind: "business", the documents
its legal form needs, and each controller with their own documents, and
we read the company and people details from them. See
Customers, and for the documents each legal form
owes, Documents we ask for.
The response embeds the endorsement with its five requirement buckets:
"endorsements": [
{
"name": "endo_usd",
"status": "incomplete",
"requirements": {
"complete": ["terms_of_service"],
"uploaded": ["identity", "proof_of_address", "selfie"],
"pending": [],
"missing": ["compliance_review"],
"issues": []
}
}
]
uploaded means we hold the documents until you submit. Do not re-send
them. compliance_review in missing is the submit itself, which you owe
next.
Set partner_reference_id to your own user id. It is unique per
environment and filterable on list endpoints, so it joins Rasto records to
yours.
Submit for verification
The first submission is explicit. We hold the documents from the
previous step until you make it. An empty body uses the default
intake_method, api, which submits everything you sent on the customer.
curl -s $BASE/api/customers/cus_.../verifications \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{}' | jq .
{
"data": [
{
"id": "ver_033y1FhTOmoW9W2jAuSvQx",
"object": "verification",
"customer_id": "cus_033y1FhTOmoW9W2jAuSvQx",
"endorsement": "endo_usd",
"purpose": "full",
"requested_info": null,
"intake_method": "api",
"status": "under_review",
"url": null,
"expires_at": null,
"rejection_reasons": null,
"created_at": "2026-08-26T09:15:32+00:00"
}
],
"has_more": false,
"next_cursor": null
}
The response is a list with one round per endorsement sent to review. The call first reads the documents and fills any field left empty. In sandbox they read as a fixed sample person, so here only the document number is filled, because the request sent everything else.
If anything required is still outstanding, or a document could not be
accepted (unreadable, expired, too old, or in another name), this returns
422 verification_incomplete and names it. Individuals owe an identity
document and a selfie, plus a proof of address when they live outside the
US. Businesses owe the documents their legal form needs, and an identity
document and proof of address for each controller (see
Documents we ask for).
Supply what it names with a PATCH /api/customers/{customer_id}, then submit
again.
Once a submission succeeds, you do not call it again for this customer. A later PATCH that changes something resubmits by itself.
Approve the decision
After the submit, endo_usd reads pending. In live the decision
arrives on a webhook. In sandbox the review stays pending until you
decide it:
curl -s $BASE/api/sandbox/customers/cus_.../verification_decisions \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"decision": "approved"}' | jq .
The decision applies to the latest submitted review. Read the endorsement back:
curl -s $BASE/api/customers/cus_... -H "Api-Key: $KEY" | jq '.endorsements'
[
{
"name": "endo_usd",
"status": "approved",
"requirements": {
"complete": ["compliance_review", "identity", "proof_of_address", "selfie", "terms_of_service"],
"uploaded": [], "pending": [], "missing": [], "issues": []
}
}
]
That approved status gates everything that follows.
Rehearse the unhappy path too
Before you move on, run a second customer through
"decision": "request_for_information". proof_of_address moves to the
issues bucket, endorsement.request_for_information fires, and the fix
is a PATCH with a new proof of address, which resubmits by itself. Your
live UI spends most of its time in this branch. See
Verification needs more information.