Skip to content

Create a direct sandbox payment

POST /v1/payments supports hosted and direct creation with a current sandbox key holding payments:write. Omit both method and msisdn for the existing hosted CREATED response. Supply method: "simulator" and a documented synthetic number for a direct PENDING_APPROVAL receipt. Payment links remain hosted-only. In both modes return_url is an absolute https URL, and any other address answers 400 INVALID_REQUEST. embedding is for hosted creation only: with method and msisdn it answers 400 INVALID_REQUEST.

Direct mode commits the original payment, reservation, event, workflow intent, authenticated actor audit and original create receipt together. A rejected admission retains none of those effects. Provider authorization happens after commit. A 201 response is not approval. Read the original payment or verify its signed webhook to learn the current outcome; never fulfill solely from a redirect or the create receipt.

Synthetic number Simulator scenario
+243810000001 Success
+243810000002 Decline
+243810000003 Expiry
+243810000004 Uncertain
+243810000005 Late success
+243810000006 Unavailable

These numbers select synthetic scenarios. They do not send a real phone prompt or authorize a live bank account. Other numbers and methods are rejected. The service converts the number to a scenario token before switch persistence and does not return it. Never send an issuer PIN, password or account credential.

The same call in Futa’s generated TypeScript client, kept for its own use and not published:

const original = await payments.createPayment({
idempotencyKey: retainedCreationKey,
createPayment: {
amount: 12500,
currency: 'CDF',
merchantReference: 'order-123',
description: 'Basket',
returnUrl: 'https://merchant.example/complete',
expiresAt: new Date(Date.now() + 600_000),
method: 'simulator',
msisdn: '+243810000001',
},
});

Persist the original key and complete request, including its original expiry, before sending. If a reply is lost or a 503 occurs, resend exactly that request/key. Do not calculate a new expiry on retry. The original pending receipt remains stable after final outcome, expiry, and service/database restarts; use GET /v1/payments/{id} for current status. Hosted and direct creation share a merchant/key namespace: changing mode, terms or scenario returns 409 IDEMPOTENCY_CONFLICT. Nothing retries a creation for you: retry only as above, with the same key and body. The server enforces the method and msisdn pair, which a client generated from the contract’s optional fields alone does not check.

The JSON body limit is 16,384 UTF-8 bytes. Duplicate/unknown fields, coercion, unpaired selectors, query parameters, caller X-Futa-* authority headers and encoded or chunked bodies are rejected. From 0.52.0, an Idempotency-Key or merchant_reference starting with sandbox-run-, whatever its letter case, returns 400 INVALID_REQUEST: the prefix is reserved for the developer console’s sandbox runs (D-034). Amounts are integral ISO minor units; CDF amounts must be divisible by 100. Preserve the existing hosted terms, HTTPS return URL and optional owned active branch.

Handle 401/403 by restoring current access, 404 by checking owned branch attribution, and 429 by waiting for quota. New direct admission can fail with 409 for disabled routes, insufficient participant capacity, expiry during admission or an archived branch, and creation with 409 for a merchant or participant limit. A 503 may hide a committed result: retain the same key. Current credentials, scope and quota are checked for every replay. Treat checkout capabilities as private and honor Cache-Control: no-store.

Once direct admission commits, the payment has been submitted and the unsubmitted-payment cancellation endpoint cannot cancel it. Live issuer contracts, real phone prompts and provider certification remain separate production gates.

From 0.44.0 the switch applies the limits of your risk profile, per currency in integer minor units, and each participant’s maximum per transaction. POST /v1/payments answers 409 with:

  • MERCHANT_LIMIT_EXCEEDED: the amount is above your limit per payment or, for a direct payment, would take the amount approved and in flight on the Kinshasa day or month past that day’s or month’s limit.
  • PARTICIPANT_LIMIT_EXCEEDED: a direct payment above the maximum per transaction of the payer’s participant.

No payment or reservation is created, and the same request meets the same limit while it stands. For PARTICIPANT_LIMIT_EXCEEDED the payer can pay through another participant; room under a day’s or month’s limit returns when an in-flight payment ends without approval, and with the next Kinshasa day or month. For a hosted payment only the limit per payment is checked at creation; the day’s and month’s limits and the participant’s maximum apply when the payer submits in checkout.

Your limits are set by Futa’s compliance or risk analysts (an approved KYB dossier sets the first ones) and are not published through the API. If they do not fit your business, contact Futa support. A merchant without a risk profile has no merchant limit. In the sandbox the limit policy (synthetic-limits-v1) and its values are synthetic, not an approved commercial or regulatory policy, and there is no live mode.