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 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.

Next

Receive a payment.