The sandbox is a separate Talli environment with its own credentials and base URL, both provided by your Talli representative. It runs the same /v3/distribution-manager API as production, with test money. To create, rename, rotate or delete sandbox credentials yourself, call the credentials endpoints on the sandbox base URL.
The sandbox adds ten endpoints that stand in for the actors your test cannot control: your bank, the bank partner, the beneficiary's bank, PayPal, a merchant charging a prepaid card, the beneficiary's mailbox, the mailbox of a colleague you invite to the console, and time passing. In the routes below, … stands for /v3/distribution-manager.
In production these routes return 404, and the API reference does not list them. The endpoint reference below documents them in full.
The ten simulate endpoints need the simulation:manage scope. The :direct-ach endpoint that this guide mentions needs the payoutInstructions:directPayout scope. Both scopes are in the Full access preset. If one of these calls returns 403, call with a Full access credential. A credential's access cannot change, so if your organization has none, a console user can create one, as Credentials describes.
These four steps are the same for every payout method. You need a distribution whose bank account is Active and which offers at least one payout method. The Distributions quickstart shows how to find your distributions.
Every distribution draws its payouts from one bank account. Read it from the distribution:
curl --request GET \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/distributions/YOUR_DISTRIBUTION_ID/account-details' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'Take id from the response — that is the bankAccountId you fund in step 2. GET …/bank-accounts lists every account you have. In production you would push a real ACH deposit to the account's routingNumber and accountNumber, or, when acceptsWires is true, send a domestic wire with accountHolderName as the beneficiary and bankName and bankAddress as the beneficiary bank; in the sandbox you call :receive-ach instead.
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/bank-accounts/YOUR_BANK_ACCOUNT_ID/:receive-ach' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{ "amount": 25000.00, "idempotencyKey": "funding-1" }'Send an idempotencyKey. A call that fails halfway can then be repeated safely: it resumes the same deposit instead of adding a second one.
The call returns 202 as soon as the bank partner accepts the deposit. Poll GET …/bank-accounts/{id} until unallocatedBalance covers the payouts you plan to send. It rises within seconds.
Fund the account whatever payout method you test. Every payout draws on it.
POST …/payout-instructions creates it, POST …/payout-instructions/{id}/:initiate sends the beneficiary a claim email. See the API reference for both request bodies. The payout instruction is now Initiated.
curl --request GET \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/emails?limit=50' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'Match the rows on recipient to find your beneficiary's mail, and take the newest InitiateEmail among them. Open the claim link in its htmlBody. Claim the payout as the beneficiary would. If the claim page asks for a one-time code, read it from the same endpoint — it arrives as an OtpEmail. Select a payout method and complete the claim.
You can also get the claim link from POST …/payout-instructions/{id}/:export-claim-link, which sends no email.
POST …/payout-instructions/{id}/:send-notification sends the same InitiateEmail again, with a new claim link; the earlier links keep working.
The payout instruction moves to Requested. What happens next depends on the payout method the beneficiary selected:
- ACH waits for the bank partner to settle it. Finish it with Finish an ACH payout.
- PayPal and Venmo wait for PayPal. Finish them with Finish a PayPal or Venmo payout.
- Gift cards are issued by the gift card provider's test environment. See Test a gift card payout.
- Prepaid cards are issued by the bank partner's test environment. Put purchases on them with Test prepaid card transactions.
- Every other payout method has no simulate call. Poll
GET …/payout-instructions/{id}and watch the status, or wait for thepayoutInstruction.updatedevent.
An ACH payout is Requested once the platform has originated it at the bank partner. Settle it:
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/payout-instructions/YOUR_PAYOUT_INSTRUCTION_ID/:settle-ach' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'A 409 with The ACH payout has not been originated at the bank partner yet means the payout has not reached the bank partner. Wait a few seconds and call again.
As in production, a settled ACH payout then waits two business days in the ACH return window, and the payout instruction stays Requested. Within that window you choose how the payout ends.
The payout succeeds. Close the return window at once instead of waiting two business days:
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/payout-instructions/YOUR_PAYOUT_INSTRUCTION_ID/:complete-ach' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'Poll GET …/payout-instructions/{id} until the status is Fulfilled, or wait for the payoutInstruction.updated event. It follows within seconds. Without this call the payout is fulfilled once the two business days have passed.
The beneficiary's bank returns the payout. Return it while it is in the return window. The body is optional: returnReasonCode is the ACH return reason the beneficiary's bank reports, one of the codes listed in ACH return reason codes. Without it the payout is returned with no reason code.
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/payout-instructions/YOUR_PAYOUT_INSTRUCTION_ID/:return-ach' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{ "returnReasonCode": "R03" }'Within seconds the amount goes back to the bank account. A payout claimed by a beneficiary moves to Reinitiated, and the beneficiary gets a new claim email (FailureReinitiateEmail in GET …/simulate/emails). A payout created with :direct-ach moves to Cancelled.
If the payout instruction has not changed within a minute, call :return-ach again. The bank partner occasionally accepts a return without applying it, and a repeated call never returns a payout twice.
A 409 with The ACH payout is not settled yet means :settle-ach has not been called, or its result has not reached the platform yet. A 409 with already fulfilled means the return window is closed: a fulfilled payout cannot be returned in the sandbox.
Call either :complete-ach or :return-ach, not both. After either call, wait until GET …/payout-instructions/{id} shows the new status: the other call made before that can leave a returned payout Fulfilled.
To test ACH payouts without the claim journey, use POST …/distributions/{id}/payout-instructions/:direct-ach. It creates the payout instruction from beneficiary and bank details you supply and sends no email, so it replaces steps 3 and 4. Settle, complete or return it the same way.
A PayPal or Venmo payout in the sandbox never reaches PayPal, so it completes only through this call:
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/payout-instructions/YOUR_PAYOUT_INSTRUCTION_ID/:settle-paypal' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'Poll GET …/payout-instructions/{id} until the status is Fulfilled, or wait for the payoutInstruction.updated event. It follows within seconds.
A 409 with The PayPal transfer is Initiated and has not been queued yet means the platform is still preparing the payout, which takes a few seconds after the claim. Wait a few seconds and call again.
Once the beneficiary has claimed the payout with a prepaid card and the card screen of the claim journey shows the card, you can put purchases on it. Copy the card number from the card screen.
A purchase within the balance. Authorize it with the card number as pan:
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/card-transactions/:authorize' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{ "pan": "YOUR_CARD_NUMBER", "amount": 25.00 }'The call answers 202 with the transactionId of the purchase. The card screen shows a pending purchase and an available balance lower by the amount within seconds. Then settle it with that transactionId:
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/card-transactions/YOUR_TRANSACTION_ID/:clear' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'The card screen shows the purchase as settled within seconds.
A declined purchase. Authorize more than the available balance. The call still answers 202 with a transactionId: the card declines the purchase, the card screen shows it as declined, and the balance does not change.
A cancelled purchase. Authorize a purchase, then void it with its transactionId while it is pending:
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/card-transactions/YOUR_TRANSACTION_ID/:void' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'The pending amount returns to the available balance. This can take up to a minute, so check the card screen again.
The card number is sent only to the bank partner and is never returned in a response. The sandbox does not check that the card belongs to your organization, so use the cards of your own payouts.
A gift card payout needs no simulate call: the sandbox orders the card from the gift card provider's test environment, as production orders it from the live one.
- Add a gift card payout method to your distribution.
GET …/payout-methodslists the methods the sandbox offers; the gift card methods carry the brand name. - Fund the bank account, create and initiate a payout instruction, and claim it as the beneficiary with the gift card method, as in Test a payout end to end.
- Poll
GET …/payout-instructions/{id}until the status isFulfilled, or wait for thepayoutInstruction.updatedevent. It usually takes seconds, at most about two minutes. - Open the card as the beneficiary: the claim page shows it, and
GET /beneficiary/gift-cardreturns its card number and PIN.
If the payout instruction is still Requested after a few minutes, the provider's test environment did not issue the card. Create a new payout instruction and claim it again.
The sandbox console is at https://console.sandbox.talli.ai. Invite a colleague with POST …/invitations/bulk, as in production. The sandbox delivers no invitation email, so read the invitation link instead:
curl --request GET \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/invitations?limit=10' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'Match the rows on invitee and open the invitationUrl. The invitee sets a password, enrols an authenticator app, and lands on the console as a member of your organization.
- A simulate call returns
202when the bank partner or the platform accepts it, not when Talli has applied the result. The result follows seconds later, so poll the resource you expect to change, or receive it as a webhook where an event exists. - Simulated settlement is instant. A real ACH deposit or payout takes a few business days.
- The ACH return window lasts two business days after settlement, as in production.
:complete-achcloses it at once. - In production, a return that arrives within two business days after the payout settles is handled automatically: the amount goes back to the bank account, a beneficiary payout is reinitiated with a new claim email, and a direct payout is cancelled. A return after that reaches a
Fulfilledpayout; contact Talli support.:return-achsimulates the first case only. - No PayPal or Venmo payout leaves the sandbox.
:settle-paypalis the only way one completes. - Prepaid card purchases come only from
:authorize, from a US merchant namedSandbox merchant. A declined or a voided purchase shows on the card screen as in production. - Gift cards come from the gift card provider's test environment: test brands only, and about one order in seventeen is never issued.
- The sandbox delivers no email. Claim links and one-time codes come from
GET …/simulate/emails, console invitation links fromGET …/simulate/invitations. - With a brand slug, claim links open at
https://<slug>.redeem.sandbox.talli.aiand the console athttps://<slug>.console.sandbox.talli.ai. Custom domains for the claim page and the console are not available on the sandbox.
POST /v3/distribution-manager/simulate/bank-accounts/{bankAccountId}/:receive-achFunds the bank account as an inbound ACH credit from your bank would: the bank partner receives, settles and releases the credit into the account.
| Field | Type | Description |
|---|---|---|
amount | decimal | Required. The amount to deposit, in dollars. Positive, at most 2 decimal places. |
idempotencyKey | string | Optional, at most 100 characters. Repeating a call with the same key, bank account and amount resumes an interrupted deposit instead of creating a second one. Without a key, every call creates a new deposit. |
| Code | Meaning |
|---|---|
202 | The bank partner accepted the deposit. Empty body. |
400 | The amount is not positive, or has more than 2 decimal places. |
404 | No bank account with this id in your organization. |
409 | The bank account is not provisioned yet, or the idempotency key was already used for a different deposit. |
500 | The bank partner rejected a step of the simulation. detail names the step. Repeat the call with the same idempotencyKey to resume the deposit. A call sent without a key cannot be resumed, and repeating it deposits the amount a second time. |
POST /v3/distribution-manager/simulate/payout-instructions/{payoutInstructionId}/:settle-achSettles the payout instruction's ACH transfer as the bank partner would. The payout then waits in the ACH return window and the payout instruction stays Requested. The request has no body.
| Code | Meaning |
|---|---|
202 | The bank partner accepted the settlement. Empty body. |
404 | No payout instruction with this id in your organization. |
409 | The payout has no ACH transfer that can be settled. detail names the current state. A payout claimed with another payout method always answers 409. |
500 | The bank partner rejected a step of the simulation. detail names the step. The call is safe to repeat. |
POST /v3/distribution-manager/simulate/payout-instructions/{payoutInstructionId}/:complete-achCloses the return window of a settled ACH payout as if two business days had passed without a return. The payout instruction then completes through the normal pipeline. The request has no body.
| Code | Meaning |
|---|---|
202 | The platform accepted the completion. Empty body. |
404 | No payout instruction with this id in your organization. |
409 | The payout is not in the return window. detail names the current state: not settled yet means call :settle-ach first or wait a few seconds after it, already fulfilled means the payout is complete, FailedPermanently or already returned means the payout was returned. |
POST /v3/distribution-manager/simulate/payout-instructions/{payoutInstructionId}/:return-achReturns a settled ACH payout inside its return window as the beneficiary's bank would. The amount goes back to the bank account through the normal pipeline. The request body is optional.
| Field | Type | Description |
|---|---|---|
returnReasonCode | string | Optional. An ACH return reason code, in capitals, for example R03. See ACH return reason codes. Without it the payout is returned with no reason code. |
| Code | Meaning |
|---|---|
202 | The bank partner accepted the return. Empty body. The bank partner applies it a few seconds later. If the payout instruction has not changed within a minute, call again: a repeated call never returns a payout twice. |
400 | returnReasonCode is not one of the accepted codes. |
404 | No payout instruction with this id in your organization. |
409 | The payout is not in the return window. detail names the current state: not settled yet means call :settle-ach first or wait a few seconds after it, already fulfilled means the window is closed and the payout cannot be returned in the sandbox, FailedPermanently means the return is complete, already returned means it is being applied. |
500 | The bank partner rejected the return. detail names the step. |
The beneficiary's bank reports why it returned a payout with a code defined by the Nacha Operating Rules. These are the codes a bank typically sends back for a payout to a beneficiary's account:
| Code | Meaning |
|---|---|
R02 | Account closed. |
R03 | No account, or the account cannot be located. |
R04 | Invalid account number structure. |
R06 | Returned at the request of the originating bank. |
R14 | Representative payee deceased or unable to continue in that capacity. |
R15 | Beneficiary or account holder deceased. |
R16 | Account frozen, or entry returned per OFAC instruction. |
R17 | File record edit reject. |
R20 | Non-transaction account. |
R23 | Credit entry refused by the receiver. |
R24 | Duplicate entry. |
:return-ach also accepts every other Nacha code from R01 to R47, R50 to R53, R61 to R77 and R80 to R85. The code does not change how the payout ends: the amount goes back to the bank account, and the payout instruction moves to Reinitiated, or to Cancelled for a :direct-ach payout.
POST /v3/distribution-manager/simulate/card-transactions/:authorizeAuthorizes a purchase on the prepaid card with this card number as a merchant would. Within the available balance the purchase is pending and the balance drops; above it the card declines the purchase and the balance does not change.
| Field | Type | Description |
|---|---|---|
pan | string | Required. The card number from the beneficiary's card screen, 13 to 19 digits, no spaces. |
amount | decimal | Required. The purchase amount, in dollars. Positive, at most 2 decimal places. |
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/card-transactions/:authorize' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{ "pan": "YOUR_CARD_NUMBER", "amount": 25.00 }'Successful response example:
{
"transactionId": "3f6c2a1e-8b4d-4c7a-9e21-5d0b7f3a9c64"
}| Code | Meaning |
|---|---|
202 | The bank partner accepted the authorization, approved or declined. transactionId identifies the purchase for :clear and :void. |
400 | pan is not 13 to 19 digits, or the amount is not positive or has more than 2 decimal places. |
404 | No card has this card number at the bank partner. Check the number on the card screen. Nothing was authorized. |
500 | The bank partner rejected the call. detail names the step. Check the card screen before you repeat it: the purchase may have been authorized, and a repeated call adds a second one. |
POST /v3/distribution-manager/simulate/card-transactions/{transactionId}/:clearSettles a pending card transaction as the merchant would. Take transactionId from the :authorize response. The request has no body.
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/card-transactions/YOUR_TRANSACTION_ID/:clear' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'| Code | Meaning |
|---|---|
202 | The bank partner accepted the settlement. Empty body. The card screen shows the purchase as settled within seconds. |
404 | The bank partner has no card transaction with this id. |
409 | The transaction is not pending. detail names its status. |
500 | The bank partner rejected the call. detail names the step. |
POST /v3/distribution-manager/simulate/card-transactions/{transactionId}/:voidCancels a pending card transaction as the merchant would: the pending amount returns to the available balance. Take transactionId from the :authorize response. The request has no body.
curl --request POST \
--url 'https://api.sandbox.talli.ai/v3/distribution-manager/simulate/card-transactions/YOUR_TRANSACTION_ID/:void' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'| Code | Meaning |
|---|---|
202 | The bank partner accepted the void. Empty body. The result can take up to a minute; check the card screen. |
404 | The bank partner has no card transaction with this id. |
409 | The transaction is not pending. detail names its status. |
500 | The bank partner rejected the call. detail names the step. |
POST /v3/distribution-manager/simulate/payout-instructions/{payoutInstructionId}/:settle-paypalCompletes the payout instruction's PayPal or Venmo transfer as PayPal would when the payout succeeds. The payout instruction then completes through the normal pipeline. No money is sent to PayPal. The request has no body.
| Code | Meaning |
|---|---|
202 | The platform accepted the completion. Empty body. |
404 | No payout instruction with this id in your organization. |
409 | The payout has no PayPal or Venmo transfer that can be completed. detail names the current state: Initiated means wait a few seconds and call again, already Processed means the payout is complete. A payout claimed with another payout method always answers 409. |
GET /v3/distribution-manager/simulate/emails?limit=10Returns your organization's most recent emails, newest first, with claim links and one-time codes, so you can test the beneficiary journey without a real mailbox. limit is between 1 and 50 and defaults to 10; a value outside that range is rejected with 400. There is no paging.
Successful response example:
[
{
"id": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"recipient": "beneficiary@example.com",
"templateType": "InitiateEmail",
"deliveryStatus": "Delivered",
"createdOn": "2026-09-09T10:15:03.412Z",
"sentOn": "2026-09-09T10:15:04.128Z",
"payoutInstructionId": "1a2b3c4d-e5f6-7890-1234-567890abcdef",
"subject": "Your payment is ready",
"htmlBody": "<html>…<a href=\"https://claim.example.talli.ai/…\">Claim your payment</a>…</html>"
}
]| Field | Description |
|---|---|
id | Identifier of the email record. |
recipient | The address the email was sent to. |
templateType | InitiateEmail, ReminderEmail, FailureReinitiateEmail, OtpEmail, PushEmail, InternalRequest or PayoutAvailableEmail. |
deliveryStatus | Queued, Sending, Sent, Delivered, Opened, Bounced, SpamComplaint, SendingFailed or Cancelled. |
createdOn | When the platform created the email. The sort field. |
sentOn | When the email was handed to the provider. For a Queued, Sending, Cancelled or SendingFailed email this is the planned send time instead. |
payoutInstructionId | The payout instruction the email belongs to, where there is one. |
subject | Subject line. |
htmlBody | HTML body, including the claim link or the one-time code. |
subject and htmlBody are null while the email content is not available. For an email sent seconds ago, call again.
GET /v3/distribution-manager/simulate/invitations?limit=10Returns your organization's pending console invitations, newest first, each with the link the invitee opens to accept it. Expired invitations are omitted. limit is between 1 and 50 and defaults to 10; a value outside that range is rejected with 400. There is no paging.
Successful response example:
[
{
"id": "uinv_OzHeJnMgHRDFMzqw",
"invitee": "colleague@example.com",
"inviter": "Talli Platform",
"createdAt": "2026-09-16T12:19:11.886Z",
"expiresAt": "2026-09-23T12:19:11.886Z",
"invitationUrl": "https://console.sandbox.talli.ai/?invitation=…&organization=…&organization_name=…"
}
]| Field | Description |
|---|---|
id | Identifier of the invitation, the same as in GET …/invitations. |
invitee | The address the invitation was sent to. |
inviter | The name shown to the invitee as the sender. |
createdAt | When the invitation was created. The sort field. |
expiresAt | When the invitation stops working. |
invitationUrl | The link that accepts the invitation. Anyone who opens it can join your sandbox organization as the invitee, so treat the response as you would a mailbox. |
Nine simulate endpoints carry their own limits, per organization; a call to one of them spends the limit below instead of the general 60 / minute default described in API Basics. The eight POST endpoints share one allowance. GET …/simulate/invitations has no limit of its own and spends the general default.
| Endpoint(s) | Limit |
|---|---|
POST …/simulate/bank-accounts/{id}/:receive-ach, :settle-ach, :complete-ach, :return-ach and :settle-paypal under POST …/simulate/payout-instructions/{id}/, and :authorize, :clear and :void under POST …/simulate/card-transactions/ | 10 / minute, shared |
GET …/simulate/emails | 120 / minute |
GET …/simulate/invitations | general default, 60 / minute |