Withdrawals
Pay out USDC on Celo in Colombian pesos to the party's own Bre-B key, with a reusable address.
With the withdraw product, a party turns USDC on Celo into Colombian pesos in its own bank
account. TuCOP gives the party a withdrawal address for one of its own Bre-B keys. Whatever
USDC arrives there is paid out in COP to that key. The address is created once and reused for every
withdrawal.
There is no exact amount to hit. The address pays out any amount above the provider's floor (4,000 COP), so a sender that cannot produce an exact figure (a swap that always lands a little over) can still use it.
Every request needs your consumer key and a party session for a party linked to your app. What else a call needs depends on the call:
| Call | Also needs |
|---|---|
POST /withdrawals, POST /quotes with product: "withdraw" | The withdraw product, and a party that finished identity verification and is not restricted. |
DELETE /withdrawals/{id} | Nothing more. The party may be unverified or restricted, and your consumer may have lost the product: an address can always be turned off. |
GET /withdrawals, GET /withdrawals/{id} | Nothing more. |
Without the product, those calls get 403 product_not_enabled. withdraw is turned on by TuCOP
only: it cannot be requested from the console. Ask at
support@tucop.org.
Register the party's own key
POST /destinationswithownership: "first_party". The Bre-B directory must report the party as the holder. Wait until it isverified. See Destinations.Get the withdrawal address
POST /withdrawalswith the destination. The first call creates the address (201); later calls return the same one (200). One address per key; a person with several keys gets one for each.Quote, if you need a figure
POST /quoteswithproduct: "withdraw"and the COP you want to arrive. Send at leastsource_amount.Send USDC
Send USDC on Celo to
deposit.address.Follow
GET /withdrawals/{id}lists the payouts made from the address, each with a status. The pesos usually reach the Bre-B key within 15 to 30 minutes after the USDC arrive; the payout iscompletedonce they were sent.
Get the withdrawal address
curl https://sandbox.api.ramp.tucop.xyz/withdrawals \
--request POST \
--header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
--header "X-TuCOPRamp-Party-Session: $PARTY_SESSION" \
--header "Idempotency-Key: $(uuidgen)" \
--header "Content-Type: application/json" \
--data '{ "destination_id": "dst_01j9z3k8q2m4n6p8r0s2t4v6w8" }'
{
"id": "wdr_01j9z3k8q2m4n6p8r0s2t4v6w8",
"product": "withdraw",
"status": "active",
"destination_id": "dst_01j9z3k8q2m4n6p8r0s2t4v6w8",
"deposit": {
"address": "0x5b1d7c0a4f2e8b9d6c3a1e0f7b2d4c6a8e9f0b1c",
"chain": "eip155:42220",
"asset": "USDC",
"token_contract": "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
"minimum_destination_amount": { "amount": "4000.00", "asset": "COP" }
},
"fee_bps": 150,
"return_address": "0x1111111111111111111111111111111111111111",
"settlement_time": { "min_minutes": 15, "max_minutes": 30 },
"disabled_at": null,
"created_at": "2026-10-10T20:00:00.000Z"
}
| Field | Meaning |
|---|---|
deposit.address | Where the party sends USDC on Celo. Reusable. |
deposit.minimum_destination_amount | The provider does not pay out below this. |
fee_bps | TuCOP's fee on this address, in basis points of what arrives. TuCOP sets it for each integrator. It is fixed when the address is created. |
status | active, or disabled after DELETE /withdrawals/{id}. |
return_address | Optional. Where the provider returns the USDC if a payout fails. It defaults to the wallet the session was opened with; pass return_address to use another wallet. Without either (for example, a session opened with email), the address has no return wallet and return_address is null: a failed payout goes to TuCOP, which returns it to the person. When you send one, your app answers for it; TuCOP checks only that it is an address. Each address keeps its own, so two keys of the same person can return to different wallets. To add or change it later, call POST /withdrawals again with the same destination_id and the new return_address: TuCOP sets it at the provider and answers 200 with the same address. If the provider cannot be reached the answer is 502 provider_unavailable and the previous one stays. It also changes when you re-enable a disabled address. |
settlement_time | How long a payout usually takes once the USDC arrive: the pesos reach the Bre-B key between min_minutes and max_minutes later. See How long it takes. |
disabled_at | When the address was disabled. null while it is active. |
The destination must be a verified first-party destination of the party. A third-party destination
answers 422 destination_not_first_party; use Payments for that. A party that stands
for a payments provider account TuCOP linked to your integrator has no identity document to match a
first-party key, so its withdraw quotes and POST /withdrawals answer
409 first_party_not_available.
Several keys per person
A person can register several of their own Bre-B keys (for example one per bank) and get a withdrawal address for each:
- Register each key with
POST /destinationsandownership: "first_party". By default there is no limit on how many keys a person has. You can set one in the integrator console (TuCOP may also set a maximum; the lower one applies); it counts the first-party keys your app registered for the person, and one more answers409 destination_limit_reached. See Destinations. - Call
POST /withdrawalsonce per destination. There is one address per destination for your integrator: calling again for the same destination returns the same address with200. - An address belongs to the integrator that created it: it carries your fee and sends you its webhooks. Another integrator linked to the same person never sees, disables or reuses it; for the same key it gets an address of its own.
- Each address pays out only to its own key. USDC sent to the address of key A always goes to key A.
- Each address has its own
return_address. They may differ between keys.
GET /withdrawals lists all the addresses, with their destination_id, so you can show the
person which address goes to which key. The list comes in data:
{
"data": [
{
"id": "wdr_01j9z3k8q2m4n6p8r0s2t4v6w8",
"product": "withdraw",
"status": "active",
"destination_id": "dst_01j9z3k8q2m4n6p8r0s2t4v6w8",
"deposit": {
"address": "0x5b1d7c0a4f2e8b9d6c3a1e0f7b2d4c6a8e9f0b1c",
"chain": "eip155:42220",
"asset": "USDC",
"token_contract": "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
"minimum_destination_amount": { "amount": "4000.00", "asset": "COP" }
},
"fee_bps": 150,
"return_address": "0x1111111111111111111111111111111111111111",
"settlement_time": { "min_minutes": 15, "max_minutes": 30 },
"disabled_at": null,
"created_at": "2026-10-10T20:00:00.000Z"
}
]
}
Disable and re-enable
DELETE /withdrawals/{id} disables an address and answers 200 with it, status: "disabled"
and disabled_at set. Disabling it again answers 200 with the same resource. It also sends a
withdrawal.disabled webhook to the consumer that created the address.
curl https://sandbox.api.ramp.tucop.xyz/withdrawals/wdr_01j9z3k8q2m4n6p8r0s2t4v6w8 \
--request DELETE \
--header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
--header "X-TuCOPRamp-Party-Session: $PARTY_SESSION"
After disabling, TuCOP keeps reading the address for 7 days, so a payout from a late deposit in
that time still arrives as withdraw.drained and shows in GET /withdrawals/{id}. After that it
is no longer followed.
To use the key again, call POST /withdrawals with the same destination_id. It re-enables the
same address (200, status: "active", disabled_at: null); no new address is created. Send
return_address to change where failed payouts are returned: TuCOP updates it at the provider too,
and if the provider cannot be reached the answer is 502 provider_unavailable and the address stays
disabled. Without return_address, the stored one is kept.
To remove the key itself, disable its withdrawal and then delete the destination with
DELETE /destinations/{id}. The destination is kept as deleted (active: false). Because the
disabled address still pays out to the key, TuCOP keeps the key at the provider. To use the key
again later, register it again with POST /destinations: the same destination comes back
(200, same destination_id, active: true, still verified), and POST /withdrawals with it
re-enables the same address. A key never gets a second address from your integrator.
Quote
A withdraw quote tells you how much USDC to send so that at least a given amount of COP arrives. It
uses the provider's rate, a 1 percent buffer for rate movement and TuCOP's fee (set by TuCOP for
each integrator), every division rounded up. The answer is 201 with the same fields as a
payment quote and product: "withdraw", settlement_time included. The
minimum is 4,000 COP.
curl https://sandbox.api.ramp.tucop.xyz/quotes \
--request POST \
--header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
--header "X-TuCOPRamp-Party-Session: $PARTY_SESSION" \
--header "Idempotency-Key: $(uuidgen)" \
--header "Content-Type: application/json" \
--data '{
"product": "withdraw",
"destination_id": "dst_01j9z3k8q2m4n6p8r0s2t4v6w8",
"amount_cop": { "amount": "100000.00", "asset": "COP" }
}'
Nothing takes a withdraw quote: it is a price, not a reservation. Sending more than source_amount
is fine; the extra is paid out too.
Follow the payouts
Every time USDC arrives at the address, the provider makes a payout: it converts the USDC and
sends the pesos to the key. GET /withdrawals/{id} returns the address with payouts, most
recent first. GET /withdrawals lists the party's withdrawal addresses your integrator created, active and disabled,
newest first, at most 50, without payouts: to see payouts, read one address by id or listen to
the webhook.
"payouts": [
{
"id": "3f9c2a1e-5b7d-4c8e-9a0b-1d2e3f4a5b6c",
"status": "completed",
"amount": { "amount": "25.123456", "asset": "USDC" },
"destination_amount": { "amount": "98500.00", "asset": "COP" },
"error_code": null,
"created_at": "2026-10-11T15:04:10.003Z",
"updated_at": "2026-10-11T15:05:10.003Z",
"completed_at": "2026-10-11T15:05:10.003Z"
}
]
| Field | Meaning |
|---|---|
id | The payout. The same value is payout_id in the webhook. |
status | See Payout status. |
amount | The USDC that arrived at the address. |
destination_amount | The COP sent to the Bre-B key. |
error_code | Why the payout failed, as provider_<reason> (the same style as a payment's error_code). null unless failed. |
created_at, updated_at | When the payout started and when it last changed. |
completed_at | When it reached completed. null until then. |
Payout status
A payout uses the same statuses, with the same meanings, as a payment.
It exists only once the USDC arrived, so it is never awaiting_funds or creating.
status | Meaning |
|---|---|
processing | The provider has the USDC and is sending the pesos. Part of the usual 15 to 30 minutes from deposit to pesos. Not a failure. |
completed | The provider sent the pesos to the Bre-B key through Bre-B. Final. |
failed | The payout could not be made. error_code says why. Final. |
refunded | The USDC went back to the withdrawal's return_address (to TuCOP when it is null). Final. |
canceled | The provider canceled the payout. Final. |
How you know the money was sent: the payout is completed. TuCOP cannot see the receiving
bank's own confirmation, but Bre-B is instant, so in practice the pesos are in the account once the
payout is completed. A payout that does not go through ends failed or refunded; when it is
refunded, the USDC returns to return_address (or to TuCOP, when it is null, which returns it to the person), so check before sending again. In the
rare case that a completed payout is returned afterwards, it reads processing while the return
is under way and ends refunded.
Webhook
Each payout status arrives as a withdraw.drained webhook, once per payout and
status, to the consumer that created the address: usually processing and then completed. A
payout that was already finished when TuCOP first read it arrives only with its final status.
{
"id": "evt_3f3gn1wv2y9e59mn9z1xn1pb1x",
"type": "withdraw.drained",
"category": "on_off_ramp",
"timestamp": "2026-10-11T15:05:40.000Z",
"api_version": "2026-10-08",
"data": {
"withdrawal_id": "wdr_01j9z3k8q2m4n6p8r0s2t4v6w8",
"destination_id": "dst_01j9z3k8q2m4n6p8r0s2t4v6w8",
"payout_id": "3f9c2a1e-5b7d-4c8e-9a0b-1d2e3f4a5b6c",
"status": "completed",
"amount": { "amount": "25.123456", "asset": "USDC" },
"destination_amount": { "amount": "98500.00", "asset": "COP" },
"error_code": null,
"created_at": "2026-10-11T15:04:10.003Z",
"updated_at": "2026-10-11T15:05:10.003Z",
"completed_at": "2026-10-11T15:05:10.003Z"
}
}
TuCOP reads the address from the provider about once a minute while it is active, so the event can
trail the payout by a minute; an address with no activity for a day is read every 30 minutes. A
disabled address is still read for 7 days after it was disabled. The same events are in
GET /events.
How long it takes
After the USDC arrive at deposit.address, the provider usually takes 15 to 30 minutes to
settle the order and the pesos reach the party's Bre-B key. Meanwhile the payout is processing.
That is normal, not a failure: do not send the USDC again. The withdraw.drained webhook tells you
when it is completed (or failed or refunded).
The withdrawal and the withdraw quote carry the expected window as settlement_time, in minutes
after the USDC arrive. Show it to the person, for example "Los pesos llegan a tu llave Bre-B entre
15 y 30 minutos después de que recibimos los USDC." Read it from the response instead of
hard-coding it: TuCOP may update it. It is an expectation, not a guarantee.
Errors
| Code | Status | When |
|---|---|---|
invalid_return_address | 400 | return_address is not a 0x address. |
invalid_amount, amount_not_settleable | 400 | A quote amount that is not whole pesos in COP. |
product_not_enabled | 403 | Your consumer does not have withdraw. |
party_restricted | 403 | TuCOP restricted the party. Creating or re-enabling an address, or a quote. |
destination_not_found, withdrawal_not_found | 404 | Not the party's, or not there. |
destination_not_verified | 409 | The directory has not matched the key yet. |
party_not_verified | 409 | Identity verification is not finished. |
first_party_not_available | 409 | Creating an address or a withdraw quote for a party that stands for a payments provider account TuCOP linked to your integrator. It has no identity document to match a first-party key. Use Payments for third-party keys. |
destination_not_first_party | 422 | The destination belongs to someone else. |
amount_below_minimum | 422 | A quote under 4,000 COP. The error carries minimum, as on a pay quote. |
provider_rejected | 422 | The provider refused to create the address. |
provider_unavailable, rate_unavailable | 502 | Retry. When re-enabling with a new return_address, the address stays disabled until a retry succeeds. |