Quickstart2. Onboard a Customer

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 $BASE set to https://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.

Next

Receive a payment.