TuCOPRamp

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:

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

  1. Register the party's own key

    POST /destinations with ownership: "first_party". The Bre-B directory must report the party as the holder. Wait until it is verified. See Destinations.

  2. Get the withdrawal address

    POST /withdrawals with 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.

  3. Quote, if you need a figure

    POST /quotes with product: "withdraw" and the COP you want to arrive. Send at least source_amount.

  4. Send USDC

    Send USDC on Celo to deposit.address.

  5. 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 is completed once they were sent.

Get the withdrawal address

bash
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" }'
json
{
  "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"
}
FieldMeaning
deposit.addressWhere the party sends USDC on Celo. Reusable.
deposit.minimum_destination_amountThe provider does not pay out below this.
fee_bpsTuCOP'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.
statusactive, or disabled after DELETE /withdrawals/{id}.
return_addressOptional. 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_timeHow 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_atWhen 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 /destinations and ownership: "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 answers 409 destination_limit_reached. See Destinations.
  • Call POST /withdrawals once per destination. There is one address per destination for your integrator: calling again for the same destination returns the same address with 200.
  • 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:

json
{
  "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.

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

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

json
"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"
  }
]
FieldMeaning
idThe payout. The same value is payout_id in the webhook.
statusSee Payout status.
amountThe USDC that arrived at the address.
destination_amountThe COP sent to the Bre-B key.
error_codeWhy the payout failed, as provider_<reason> (the same style as a payment's error_code). null unless failed.
created_at, updated_atWhen the payout started and when it last changed.
completed_atWhen 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.

statusMeaning
processingThe provider has the USDC and is sending the pesos. Part of the usual 15 to 30 minutes from deposit to pesos. Not a failure.
completedThe provider sent the pesos to the Bre-B key through Bre-B. Final.
failedThe payout could not be made. error_code says why. Final.
refundedThe USDC went back to the withdrawal's return_address (to TuCOP when it is null). Final.
canceledThe 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.

json
{
  "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

CodeStatusWhen
invalid_return_address400return_address is not a 0x address.
invalid_amount, amount_not_settleable400A quote amount that is not whole pesos in COP.
product_not_enabled403Your consumer does not have withdraw.
party_restricted403TuCOP restricted the party. Creating or re-enabling an address, or a quote.
destination_not_found, withdrawal_not_found404Not the party's, or not there.
destination_not_verified409The directory has not matched the key yet.
party_not_verified409Identity verification is not finished.
first_party_not_available409Creating 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_party422The destination belongs to someone else.
amount_below_minimum422A quote under 4,000 COP. The error carries minimum, as on a pay quote.
provider_rejected422The provider refused to create the address.
provider_unavailable, rate_unavailable502Retry. When re-enabling with a new return_address, the address stays disabled until a retry succeeds.