Skip to main content

Before You Start

You will need:
  • Sandbox API credentials: A client_id and client_secret provided by your Lead Technical Account Manager during kickoff.
  • curl: Installed on macOS by default; curl --version should print something. Any HTTP client works.
  • Optional: jq— for cleaner JSON output. Pipe responses through | jq . if you have it installed.
Sandbox OnlyEvery example uses api.sandbox.lead.bank. The same calls work against api.lead.bank in production once your program is approved for go-live, but you should never run this guide against production credentials.

Steps

15 minutes We’ll go through a simple flow to authenticate against Lead, create an entity, assigned an account number, simulated an incoming ACH credit, and send an outgoing ACH.
Lead uses OAuth2 client credentials. You exchange your client_id and client_secret for a short-lived access token (24-hour expiry by default) and pass that token as a Bearer header on every subsequent request.Replace YOUR_CLIENT_ID and YOUR_CLIENT_SECRET with the values from your Technical Account Manager.
curl -X POST https://api.sandbox.lead.bank/v1/oauth/token \
  -H 'Content-Type: application/json' \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "scope": "entity/read_write ach/read_write account_number/read_write"
  }'
You should see a response like this:
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "scope": "entity/read_write ach/read_write account_number/read_write",
  "expires_in": 86400
}
Save the access_token value as a shell variable so the next steps are easy to copy:
export LEAD_TOKEN="eyJhbGciOiJSUzI1NiIs..."
ScopesAccess tokens are scoped. Request only the scopes you need — and you can request a narrower set later for production.
Every transaction at Lead starts with creating a representation for your customer in Lead’s system. For illustrative purposes, the request below creates an example Individual Entity with minimal information provided.
curl -X POST https://api.sandbox.lead.bank/v0/entities \
  -H "Authorization: Bearer $LEAD_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: quickstart-entity-001' \
  -d '{
    "type": "individual",
    "client_customer_id": "customer-id-from-my-system",
    "intended_roles": ["account_holder"],
    "risk_score": "low",
    "individual_details": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "date_of_birth": "1990-12-10",
      "identification_details": {
        "type": "us_entity",
        "tax_identification": { "type": "ssn", "value": "123456789" }
      },
      "contact_methods": [
        { "type": "email", "email": "ada@example.com" }
      ],
      "address_details": {
        "physical_address": {
          "line_1": "123 Analytical Engine Way",
          "city": "Brooklyn",
          "state": "NY",
          "postal_code": "11201",
          "country": "US"
        }
      }
    },
    "kyc_details": { "result": "passed", "screened_at": "2026-01-15T10:00:00Z" },
    "ofac_details": { "result": "passed", "screened_at": "2026-01-15T10:00:00Z" }
  }'
A successful response includes an id  that starts with entity_. Save it for the next step:
export LEAD_ENTITY_ID="entity_abc123..."
IdempotencyEvery write endpoint requires an Idempotency-Key header. If you retry the same request with the same key (e.g. after a network blip), Lead returns the original response instead of creating a duplicate. Use a fresh key per logical operation.
Confirm the entity is queryable. You can fetch by Lead’s entity ID you supplied.
curl https://api.sandbox.lead.bank/v0/entities/$LEAD_ENTITY_ID \
  -H "Authorization: Bearer $LEAD_TOKEN"
You should get back the same entity object you just created, with timestamps populated.
Before you can receive or send money, you need an Account Number assigned to your account. Lead provides an account_id during onboarding — this is your FBO account, not a per-user account. Use it directly to create an Account Number for each customer.Create the account number:
curl -X POST https://api.sandbox.lead.bank/v1/account_number \
  -H "Authorization: Bearer $LEAD_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: quickstart-acct-num-001' \
  -d '{
    "account_id": "YOUR_ACCOUNT_ID"
  }'
Replace YOUR_ACCOUNT_ID with the account_id provided by your Technical Account Manager.A successful response includes an id that starts with account_number_xyz and the 10-digit DDA number alongside Lead’s routing number. Save the ID — you’ll use it in the next two steps:
export LEAD_ACCT_NUM_ID="account_number_xyz..."
Account Numbers and RoutingLead assigns a real account number and routing number to every Account Number object. This is what counterparties use to send money to your customer. Multiple Account Numbers can map to a single Account — for example, one for ACH and one for wire.
The sandbox provides simulation endpoints that are not available in production. They let you trigger inbound transactions so you can test the full lifecycle of a payment. The example below is for ACH, but  wires, and instant payments are similar.
curl -X POST https://api.sandbox.lead.bank/v1/simulate/ach/incoming_ach \
  -H "Authorization: Bearer $LEAD_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: quickstart-incoming-ach-001' \
  -d '{
    "account_number_id": "'$LEAD_ACCT_NUM_ID'",
    "amount": 12500,
    "currency_code": "USD",
    "transaction_type": "credit",
    "sec_code": "PPD",
    "statement_descriptor": "DEPOSIT",
    "sender_name": "Quickstart Co",
    "sender_company_id": "1234567890",
    "sender_routing_number": 121000248
  }'
The response includes an id that starts with ach_. Copy it and substitute it for {ach_id} in the retrieve call below. Amounts are in cents — 12500 is $125.00.
curl https://api.sandbox.lead.bank/v1/ach/{ach_id} \
  -H "Authorization: Bearer $LEAD_TOKEN"
SandboxACH transactions don’t advance through their lifecycle on real network timing in sandbox. Use POST /v1/simulations/advance_ach to move the transfer through its states manually.
Now send money out from your account. An outgoing ACH requires an Account Number on the sending side and a destination routing number and account number on the receiving side.
curl -X POST https://api.sandbox.lead.bank/v1/ach \
  -H "Authorization: Bearer $LEAD_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: quickstart-outgoing-ach-001' \
  -d '{
    "account_number_id": "'$LEAD_ACCT_NUM_ID'",
    "amount": 5000,
    "currency_code": "USD",
    "transaction_type": "debit",
    "delivery_type": "next_business_day",
    "sec_code": "PPD",
    "statement_descriptor": "PAYMENT",
    "counterparty": {
      "name": "Jordan Smith",
      "routing_number": "021000021",
      "account_number": "9900000001",
      "account_type": "checking"
    }
  }'
A successful response returns an id that starts with ach_ and an initial status of created. Save it:
export LEAD_ACH_ID="ach_abc123..."
Retrieve the transfer to confirm its status:
curl https://api.sandbox.lead.bank/v1/ach/{id} \
  -H "Authorization: Bearer $LEAD_TOKEN"
Effective Entry DateSetting delivery_type to next_business_day schedules the ACH for the next valid settlement date. Use same_business_day for same-day ACH if your program supports it. Lead derives the effective date automatically — see ACH → Processing Windows for cutoff times and valid date rules.
SandboxOutgoing ACH transfers don’t submit to the real network. Use POST /v1/simulate/ach/{ach_id}/advance to move the transfer through its lifecycle states manually.

What’s Next

  • Webhooks: Receive events instead of polling. Configure your endpoint and start listening for ach.created, ach.posted, and lifecycle events for every other object.
  • Idempotency & Errors: Error envelope, retry semantics, and the canonical error codes you’ll handle.
  • Move Money Guides: Full per-rail walkthroughs for ACH, Wire, Instant Payments, Blockchain, and Internal Transfers.

Troubleshooting

Error

Potential Fix

401 Unauthorized
  • Token expired (24h default). Run Step 1 again.
  • Authorization header malformed. Should be exactly Authorization: Bearer <token> — note the space and capitalization.
403 source_ip_not_allowed
  • Your client’s public IP isn’t on the program allowlist.
  • Either add your office or VPN egress IP via your , or run the request from an allowlisted environment.
  • See Auth & Security → IP Allowlist.
422 Unprocessable Entity on Create Entity
  • Most common cause: missing or malformed kyc_details / ofac_details. Sandbox accepts placeholder values, but they must be present and structurally valid.
  • Second most common: intended_roles mismatched with entity type. For business or sole_prop, only account_holder is valid.
409 Conflict
  • You reused an Idempotency-Key with a different request body.
  • Either send the same body (and you’ll get the original response back), or pick a new key.