Dev Portal
Sandbox operational5 APIs5 groups11 operations

Sandbox

How the sandbox works.

The sandbox answers every API in the catalog with scripted outcomes. You choose the outcome with a value in your request, or with one header. No money moves, and you can test each failure without asking anyone to set it up.

01Keys and tokens

Keys, then a token, then the call

Every sandbox call carries a bearer token. You get the token by sending your app's consumer key and secret to the token endpoint.

Sandbox base URLhttps://devportal.softwaregroup.com/api/sandbox

  1. 01

    Create an app

    In Console, create an app and add an API group to it. The app gets a consumer key and a secret. Keep the secret on your server.

  2. 02

    Swap the keys for a token

    Send the key and secret with HTTP Basic auth to POST /oauth/v1/token. The answer holds an access_token that lasts one hour.

  3. 03

    Call an API

    Send Authorization: Bearer <token> on every call. When the hour is up the sandbox answers 401 INVALID_TOKEN. Request a new token and carry on.

curl -X POST 'https://devportal.softwaregroup.com/api/sandbox/oauth/v1/token' \
  -u "$LANGO_KEY:$LANGO_SECRET" \
  -d 'grant_type=client_credentials'

Token response200

{
  "access_token": "lgt_4k8w…",
  "token_type": "Bearer",
  "expires_in": 3600
}
curl -X POST 'https://devportal.softwaregroup.com/api/sandbox/collect/v1/push' \
  -H "Authorization: Bearer $LANGO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "shortCode": "600100",
    "amount": 1500,
    "currency": "KES",
    "phoneNumber": "254712345678",
    "reference": "INV-2041",
    "description": "Order 2041",
    "callbackUrl": "https://example.com/hooks/lango"
  }'

02Scenarios and magic values

A value in your request picks the outcome

Each operation has a list of named scenarios, written in its OpenAPI spec. Most have a trigger: one field set to one exact value. We call that a magic value.

The sandbox reads your request and runs the first scenario whose trigger matches. If none match, it runs the default. Values are compared as text, so 1 and "1" both match.

The table lists 20 scenarios across 4 APIs, read from the live catalog. Every reference page has the same list for its own operations.

Collect v1.2.0

Reference

POST/collect/v1/push

  • Customer pays202callback in 4.0 sdefault · any other requestpaid
  • Insufficient funds202callback in 3.0 sbody.amount = 1insufficient_funds
  • Customer cancels the prompt202callback in 2.5 sbody.amount = 2cancelled
  • Customer never responds202callback in 9.0 sbody.amount = 3no_answer
  • Invalid phone number400body.phoneNumber = "254700000000"invalid_phone

GET/collect/v1/push/{checkoutId}

  • Completed200default · any other requestcompleted
  • Unknown id404path.checkoutId = "chk_missing"not_found

Payouts v1.0.3

Reference

POST/payouts/v1/send

  • Payout succeeds202callback in 3.5 sdefault · any other requestsent
  • Float too low202callback in 2.0 sbody.amount = 1float_low
  • Duplicate reference409body.reference = "DUPLICATE"duplicate

GET/payouts/v1/{payoutId}

  • Delivered200default · any other requestdelivered

Identity Check v1.1.0

Reference

POST/identity/v1/verifications

  • Everything matches200default · any other requestmatch
  • Name does not match200body.idNumber = "00000001"name_mismatch
  • ID not on the register200body.idNumber = "00000002"not_found
  • Malformed ID number422body.idNumber = "ABC"invalid_id

GET/identity/v1/verifications/{verificationId}

  • Found200default · any other requestfound

SMS v1.4.1

Reference

POST/messaging/v1/sms

  • Delivered202callback in 2.5 sdefault · any other requestdelivered
  • Handset switched off202callback in 6.0 sbody.to = "254700000001"handset_off
  • Unregistered sender ID403body.senderId = "UNKNOWN"bad_sender

GET/messaging/v1/sms/{messageId}

  • Delivered200default · any other requestdelivered

03Forcing an outcome

Or name the scenario in a header

Sometimes you cannot change the request. Your test data is fixed, or the amount has to be a real one. Send the scenario's key in the X-Lango-Scenario header and leave the body alone.

Each scenario's key is listed next to it in the table above.

curl -i -X POST 'https://devportal.softwaregroup.com/api/sandbox/collect/v1/push' \
  -H "Authorization: Bearer $LANGO_TOKEN" \
  -H "X-Lango-Scenario: insufficient_funds" \
  -H "Content-Type: application/json" \
  -d '{
    "shortCode": "600100",
    "amount": 1500,
    "currency": "KES",
    "phoneNumber": "254712345678",
    "reference": "INV-2041",
    "description": "Order 2041",
    "callbackUrl": "https://example.com/hooks/lango"
  }'
The header wins
If the header names a scenario of that operation, that scenario runs. Magic values in the request are ignored for that call.
Unknown keys are skipped
If the key does not belong to the operation, the sandbox carries on as if the header were not there: first matching trigger, then the default.
The answer says what ran
Every sandbox response has an x-lango-scenario header with the key of the scenario that produced it. The same key is stored with the call in your request log.

04Callbacks

Results that arrive later

Some operations only confirm that work has started. The result is posted to your server a few seconds later. In the table above those scenarios are marked with a callback time.

The sandbox sends these callbacks for real, over the network, to the address you give it.

Where it goes
To the callbackUrl in the request body. If the request has none, to the default callback URL saved on the app. If neither is an http or https address, nothing is sent.
What we send
A POST with a JSON body. The x-lango-delivery header carries the delivery id. We wait up to ten seconds for your server to answer.
What is recorded
Every delivery is kept with the address, the body, the status your server answered and the number of attempts. Any 2xx counts as delivered. Anything else, or no answer, is recorded as failed with the reason.
Sending it again
The sandbox does not retry on its own. Open Console, Callbacks, open the delivery and choose Send it again. The same body goes to the same address.
No public server yet
Each app has an inspector address, https://devportal.softwaregroup.com/api/inspect/<appId>. It answers 200 to any POST. Use it as your callbackUrl, then read what arrived under Console, Callbacks. The full address is on the app's page in Console.

Callback · scenario paid4.0 s after the call

POST https://example.com/hooks/lango
content-type: application/json
user-agent: Lango-Sandbox/1.0
x-lango-delivery: whk_…
{
  "checkoutId": "chk_af1qjaex",
  "reference": "INV-2041",
  "resultCode": 0,
  "resultDescription": "Payment received.",
  "amount": 1500,
  "receipt": "RCT_3kx2ccq8",
  "phoneNumber": "254712345678",
  "paidAt": "2026-09-28T09:53:16.566Z"
}

05Limits and errors

What stops a call, and what to do

The gateway checks each call in the order shown and stops at the first problem. Only after all of them pass does a scenario run.

Every call that passes the token check is written to your request log with its request, response and status. That includes the calls that fail.

60 a minute
Rate limit on the Sandbox plan, where new subscriptions start
1 hour
Life of a token. There is no refresh. Ask for a new one.
Per app
Calls are counted per app in fixed one-minute windows. Tool calls over MCP count too.
  • 401INVALID_CLIENT

    The token request had no Basic auth header, or the key and secret do not match an active key pair.

    Check the pair in Console. A revoked pair stops working at once.

  • 401MISSING_TOKEN

    The call had no Authorization: Bearer header.

    Send the token on every call.

  • 401INVALID_TOKEN

    The token is unknown, more than one hour old, or its key pair was revoked.

    Request a new token and send the call again.

  • 403APP_SUSPENDED

    An operator has suspended the app.

    Write to developers@lango.example.

  • 404NO_SUCH_OPERATION

    The method and path match nothing in the catalog.

    Compare the path with the reference. It starts right after /api/sandbox.

  • 403NOT_SUBSCRIBED

    The app has no subscription to a group that contains this API.

    Open the app in Console and add the group.

  • 403SUBSCRIPTION_PENDING

    The group needs an operator review and your subscription is still waiting for it.

    Wait for the review. Today this applies to Treasury. Every other group works as soon as you add it.

  • 429RATE_LIMITED

    The app made more calls in one minute than its plan allows.

    Wait for the next minute, then send the call again.

Every gateway error has this shape403

{
  "error": "NOT_SUBSCRIBED",
  "message": "This app is not subscribed to an API group that contains this API.",
  "requestId": "req_7d2k9m4xqa"
}

06Try it

Run one here

This panel runs the same scenario engine as the sandbox, without keys. Pick an outcome and send it. Watch the request change: choosing insufficient funds sets amount = 1.

Nothing you send from here is stored and no callback leaves the portal. The panel shows the callback body your server would receive.

Live sandbox · Send a payment promptno keys needed
POST/collect/v1/push
{
  "shortCode": "600100",
  "amount": 1500,
  "currency": "KES",
  "phoneNumber": "254712345678",
  "reference": "INV-2041",
  "description": "Order 2041",
  "callbackUrl": "https://example.com/hooks/lango"
}

Choose what happens

Get keys and make the first call.

Sign up, create an app and add a group. Groups work in the sandbox the moment you add them, apart from Treasury, which an operator reviews first.