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
- 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.
- 02
Swap the keys for a token
Send the key and secret with HTTP Basic auth to
POST /oauth/v1/token. The answer holds anaccess_tokenthat lasts one hour. - 03
Call an API
Send
Authorization: Bearer <token>on every call. When the hour is up the sandbox answers401 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
ReferencePOST/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
ReferencePOST/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
ReferencePOST/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
ReferencePOST/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-scenarioheader 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
callbackUrlin the request body. If the request has none, to the default callback URL saved on the app. If neither is anhttporhttpsaddress, nothing is sent. - What we send
- A
POSTwith a JSON body. Thex-lango-deliveryheader 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
2xxcounts 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 answers200to any POST. Use it as yourcallbackUrl, 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.
- 401
INVALID_CLIENTThe 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.
- 401
MISSING_TOKENThe call had no
Authorization: Bearerheader.Send the token on every call.
- 401
INVALID_TOKENThe token is unknown, more than one hour old, or its key pair was revoked.
Request a new token and send the call again.
- 403
APP_SUSPENDEDAn operator has suspended the app.
Write to developers@lango.example.
- 404
NO_SUCH_OPERATIONThe method and path match nothing in the catalog.
Compare the path with the reference. It starts right after
/api/sandbox. - 403
NOT_SUBSCRIBEDThe app has no subscription to a group that contains this API.
Open the app in Console and add the group.
- 403
SUBSCRIPTION_PENDINGThe 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.
- 429
RATE_LIMITEDThe 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.
{
"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.