Simulators
Destination charges
A destination charge takes a payment on your platform and transfers a share to a connected seller, so you can test what each side’s balance records. Onboard a seller, then pay it $100.00 with a $10.00 application fee:
curl -s "$RYSTIC_URL/v2/core/accounts" -u sk_test_123: -H 'Content-Type: application/json' -d '{"dashboard":"none","identity":{"country":"us","entity_type":"individual","individual":{"given_name":"Jenny","surname":"Rosen","date_of_birth":{"day":1,"month":1,"year":1901},"address":{"line1":"address_full_match","city":"San Francisco","state":"CA","postal_code":"94103","country":"us"},"id_numbers":[{"type":"us_ssn","value":"000000000"}]},"attestations":{"terms_of_service":{"account":{"date":"2026-01-01T00:00:00.000Z","ip":"203.0.113.7"}}}},"defaults":{"currency":"usd","responsibilities":{"fees_collector":"application","losses_collector":"application"},"profile":{"business_url":"https://example.com"}},"configuration":{"merchant":{"capabilities":{"card_payments":{"requested":true}}},"recipient":{"capabilities":{"stripe_balance":{"stripe_transfers":{"requested":true}}}}}}' | jq -c '{id}'
# -> {"id":"acct_0000000000000001"}
curl -s "$RYSTIC_URL/v1/payment_intents" -u sk_test_123: -d amount=10000 -d currency=usd -d 'payment_method_types[]=card' -d payment_method=pm_card_bypassPending -d confirm=true -d 'transfer_data[destination]=acct_0000000000000001' -d application_fee_amount=1000 | jq -c '{status, latest_charge}'
# -> {"status":"succeeded","latest_charge":"ch_000000000000000000000001"}
curl -s "$RYSTIC_URL/v1/charges/ch_000000000000000000000001" -u sk_test_123: | jq -c '{transfer, application_fee, transfer_group}'
# -> {"transfer":"tr_000000000000000000000001","application_fee":"fee_000000000000000000000001","transfer_group":"group_pi_000000000000000000000001"}
curl -s "$RYSTIC_URL/v1/balance_transactions" -u sk_test_123: -H 'Stripe-Account: acct_0000000000000001' | jq -c '[.data[] | {type, amount, fee, net}]'
# -> [{"type":"payment","amount":10000,"fee":1000,"net":9000}]
Amounts are in the currency’s smallest unit, as on Stripe, so 10000 is $100.00. The bypassPending card puts what the seller gains, its share less the fee, straight into its available balance, ready for a payout. Connect covers creating the seller and acting as it.
Refund part of a destination charge
Refund half the payment, taking the seller’s share of it back and returning that part of the fee:
curl -s "$RYSTIC_URL/v1/refunds" -u sk_test_123: -d charge=ch_000000000000000000000001 -d amount=5000 -d reverse_transfer=true -d refund_application_fee=true | jq -c '{status, transfer_reversal}'
# -> {"status":"succeeded","transfer_reversal":"trr_000000000000000000000001"}
curl -s "$RYSTIC_URL/v1/balance_transactions" -u sk_test_123: -H 'Stripe-Account: acct_0000000000000001' | jq -c '[.data[] | {type, amount}]'
# -> [{"type":"adjustment","amount":500},{"type":"payment_refund","amount":-5000},{"type":"payment","amount":10000}]
curl -s "$RYSTIC_URL/v1/balance" -u sk_test_123: -H 'Stripe-Account: acct_0000000000000001' | jq -c '{available: .available[0].amount}'
# -> {"available":4500}
Each route that creates a refund takes reverse_transfer and refund_application_fee on a destination charge.
reverse_transfer=true takes the refund’s proportion of the share back from the account, and the refund’s transfer_reversal names a trr_ reversal. The platform lists a transfer_refund row. The account lists a payment_refund row and a pyr_ refund of its py_ payment, whose amount_refunded grows.
refund_application_fee=true also returns the refund’s proportion of the fee, as an application_fee_refund row on the platform and an adjustment row on the account. Each refund’s shares round half up on their own. The refund that leaves the transfer fully reversed returns the rest of the fee, as Stripe’s do.
Without refund_application_fee the account keeps paying the fee, so its balance can go negative, as Stripe’s does. Without reverse_transfer the platform alone bears the refund.
Differs from Stripe: the simulator writes none of the reserve_transaction rows Stripe posts on the platform when a refund leaves the connected account’s balance negative.
Differs from Stripe: the simulator accepts a refund whose reversal rounds to 0 and a fee refund without reverse_transfer. Stripe refuses both, the first with “You cannot reverse a transfer for amount=0.”
Pay out to a seller
Give the seller a bank account, then pay out $40.00 of its balance:
curl -s "$RYSTIC_URL/v1/accounts/acct_0000000000000001/external_accounts" -u sk_test_123: -d external_account=btok_us_verified | jq -c '{id}'
# -> {"id":"ba_1Sim00000000000000000001"}
curl -s "$RYSTIC_URL/v1/payouts" -u sk_test_123: -H 'Stripe-Account: acct_0000000000000001' -d amount=4000 -d currency=usd | jq -c '{status, destination}'
# -> {"status":"pending","destination":"ba_1Sim00000000000000000001"}
curl -s "$RYSTIC_URL/v1/balance" -u sk_test_123: -H 'Stripe-Account: acct_0000000000000001' | jq -c '{available: .available[0].amount}'
# -> {"available":500}
A seller’s payout draws on its own balance, and its destination is the bank account the external account route returned. Balance and payouts covers payouts.
The charge and the transfer
A PaymentIntent with transfer_data[destination] is a destination charge. It’s paid when created with confirm=true, when confirmed or when captured, POST /v1/charges/{charge}/capture of its charge included. It credits the platform the charge’s net as any card payment does. Then it transfers a share to the connected account: transfer_data[amount] when set, capped at the captured amount, or else the whole amount.
The account gains the share less any application_fee_amount. So with no application fee on a whole-amount transfer, the platform loses the card fee. transfer_data[amount] beside application_fee_amount returns 400 parameters_exclusive, as on Stripe. POST /v1/charges takes no Connect parameters.
The charge names these at once: destination, transfer_group (group_ plus the PaymentIntent’s id), its transfer and, with an application fee, its application_fee. No route reads the transfer or the fee.
Each side lists its own balance transactions:
| Account | Balance transactions |
|---|---|
| the platform | the charge row; a transfer row for minus the share, its source the transfer transfer.created announces; with a fee, an application_fee row for plus the fee |
| the connected account | a payment row: the share as its amount, the application fee as its fee (one application_fee line in fee_details) and the credit as its net |
The account’s payment row has the py_ payment as its source. The account reads the payment with GET /v1/charges/{id} and lists it with GET /v1/charges. The payment sends payment.created as the account’s, with request {id: null, idempotency_key: null}. That event lists among the account’s events and reaches the platform’s Connect endpoints, never the platform’s own list.
Differs from Stripe: the simulator names the transfer and the fee, moves both balances and lists the event at the charge. Stripe names the transfer and the fee about 6 seconds later, and does the rest seconds later.