Skip to content
Last updated

Sandbox testing

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.

EndpointStands in for
POST …/simulate/bank-accounts/{bankAccountId}/:receive-achYour bank sending an inbound ACH credit to a Talli bank account.
POST …/simulate/payout-instructions/{payoutInstructionId}/:settle-achThe bank partner settling an ACH payout.
POST …/simulate/payout-instructions/{payoutInstructionId}/:complete-achThe ACH return window passing without a return.
POST …/simulate/payout-instructions/{payoutInstructionId}/:return-achThe beneficiary's bank returning a settled ACH payout.
POST …/simulate/payout-instructions/{payoutInstructionId}/:settle-paypalPayPal paying out a PayPal or Venmo payout.
POST …/simulate/card-transactions/:authorizeA merchant authorizing a purchase on a prepaid card.
POST …/simulate/card-transactions/{transactionId}/:clearThe merchant settling a pending card purchase.
POST …/simulate/card-transactions/{transactionId}/:voidThe merchant cancelling a pending card purchase.
GET …/simulate/emailsThe beneficiary's mailbox.
GET …/simulate/invitationsThe mailbox of a colleague invited to the console.

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.

Test a payout end to end

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.

1. Find the bank account to fund

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.

2. Fund the bank account

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.

3. Create and initiate a payout instruction

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.

4. Claim the payout as the beneficiary

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:

Finish an ACH payout

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.

Finish a PayPal or Venmo payout

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.

Test prepaid card transactions

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.

Test a gift card payout

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.

  1. Add a gift card payout method to your distribution. GET …/payout-methods lists the methods the sandbox offers; the gift card methods carry the brand name.
  2. 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.
  3. Poll GET …/payout-instructions/{id} until the status is Fulfilled, or wait for the payoutInstruction.updated event. It usually takes seconds, at most about two minutes.
  4. Open the card as the beneficiary: the claim page shows it, and GET /beneficiary/gift-card returns 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.

Invite a colleague to the sandbox console

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.

How the sandbox differs from production

  • A simulate call returns 202 when 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-ach closes 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 Fulfilled payout; contact Talli support. :return-ach simulates the first case only.
  • No PayPal or Venmo payout leaves the sandbox. :settle-paypal is the only way one completes.
  • Prepaid card purchases come only from :authorize, from a US merchant named Sandbox 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 from GET …/simulate/invitations.
  • With a brand slug, claim links open at https://<slug>.redeem.sandbox.talli.ai and the console at https://<slug>.console.sandbox.talli.ai. Custom domains for the claim page and the console are not available on the sandbox.

Endpoint reference

Simulate an incoming ACH deposit

POST /v3/distribution-manager/simulate/bank-accounts/{bankAccountId}/:receive-ach

Funds the bank account as an inbound ACH credit from your bank would: the bank partner receives, settles and releases the credit into the account.

FieldTypeDescription
amountdecimalRequired. The amount to deposit, in dollars. Positive, at most 2 decimal places.
idempotencyKeystringOptional, 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.
CodeMeaning
202The bank partner accepted the deposit. Empty body.
400The amount is not positive, or has more than 2 decimal places.
404No bank account with this id in your organization.
409The bank account is not provisioned yet, or the idempotency key was already used for a different deposit.
500The 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.

Simulate the settlement of an ACH payout

POST /v3/distribution-manager/simulate/payout-instructions/{payoutInstructionId}/:settle-ach

Settles 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.

CodeMeaning
202The bank partner accepted the settlement. Empty body.
404No payout instruction with this id in your organization.
409The payout has no ACH transfer that can be settled. detail names the current state. A payout claimed with another payout method always answers 409.
500The bank partner rejected a step of the simulation. detail names the step. The call is safe to repeat.

Simulate the end of the ACH return window

POST /v3/distribution-manager/simulate/payout-instructions/{payoutInstructionId}/:complete-ach

Closes 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.

CodeMeaning
202The platform accepted the completion. Empty body.
404No payout instruction with this id in your organization.
409The 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.

Simulate the return of an ACH payout

POST /v3/distribution-manager/simulate/payout-instructions/{payoutInstructionId}/:return-ach

Returns 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.

FieldTypeDescription
returnReasonCodestringOptional. 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.
CodeMeaning
202The 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.
400returnReasonCode is not one of the accepted codes.
404No payout instruction with this id in your organization.
409The 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.
500The bank partner rejected the return. detail names the step.

ACH return reason codes

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:

CodeMeaning
R02Account closed.
R03No account, or the account cannot be located.
R04Invalid account number structure.
R06Returned at the request of the originating bank.
R14Representative payee deceased or unable to continue in that capacity.
R15Beneficiary or account holder deceased.
R16Account frozen, or entry returned per OFAC instruction.
R17File record edit reject.
R20Non-transaction account.
R23Credit entry refused by the receiver.
R24Duplicate 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.

Simulate a purchase authorization on a prepaid card

POST /v3/distribution-manager/simulate/card-transactions/:authorize

Authorizes 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.

FieldTypeDescription
panstringRequired. The card number from the beneficiary's card screen, 13 to 19 digits, no spaces.
amountdecimalRequired. 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"
}
CodeMeaning
202The bank partner accepted the authorization, approved or declined. transactionId identifies the purchase for :clear and :void.
400pan is not 13 to 19 digits, or the amount is not positive or has more than 2 decimal places.
404No card has this card number at the bank partner. Check the number on the card screen. Nothing was authorized.
500The 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.

Simulate the settlement of a prepaid card transaction

POST /v3/distribution-manager/simulate/card-transactions/{transactionId}/:clear

Settles 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'
CodeMeaning
202The bank partner accepted the settlement. Empty body. The card screen shows the purchase as settled within seconds.
404The bank partner has no card transaction with this id.
409The transaction is not pending. detail names its status.
500The bank partner rejected the call. detail names the step.

Simulate the void of a prepaid card transaction

POST /v3/distribution-manager/simulate/card-transactions/{transactionId}/:void

Cancels 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'
CodeMeaning
202The bank partner accepted the void. Empty body. The result can take up to a minute; check the card screen.
404The bank partner has no card transaction with this id.
409The transaction is not pending. detail names its status.
500The bank partner rejected the call. detail names the step.

Simulate the completion of a PayPal or Venmo payout

POST /v3/distribution-manager/simulate/payout-instructions/{payoutInstructionId}/:settle-paypal

Completes 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.

CodeMeaning
202The platform accepted the completion. Empty body.
404No payout instruction with this id in your organization.
409The 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.

Read the emails the platform sent

GET /v3/distribution-manager/simulate/emails?limit=10

Returns 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>"
  }
]
FieldDescription
idIdentifier of the email record.
recipientThe address the email was sent to.
templateTypeInitiateEmail, ReminderEmail, FailureReinitiateEmail, OtpEmail, PushEmail, InternalRequest or PayoutAvailableEmail.
deliveryStatusQueued, Sending, Sent, Delivered, Opened, Bounced, SpamComplaint, SendingFailed or Cancelled.
createdOnWhen the platform created the email. The sort field.
sentOnWhen the email was handed to the provider. For a Queued, Sending, Cancelled or SendingFailed email this is the planned send time instead.
payoutInstructionIdThe payout instruction the email belongs to, where there is one.
subjectSubject line.
htmlBodyHTML 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.

Read the pending console invitations

GET /v3/distribution-manager/simulate/invitations?limit=10

Returns 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=…"
  }
]
FieldDescription
idIdentifier of the invitation, the same as in GET …/invitations.
inviteeThe address the invitation was sent to.
inviterThe name shown to the invitee as the sender.
createdAtWhen the invitation was created. The sort field.
expiresAtWhen the invitation stops working.
invitationUrlThe 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.

Rate limits

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/emails120 / minute
GET …/simulate/invitationsgeneral default, 60 / minute