TuCOPRamp

Destinations

Register Bre-B keys for a party, wait for the Bre-B directory, and confirm the holder.

A destination is a Bre-B key registered for a party. TuCOP registers it with its payment provider and checks it against the Bre-B directory, which returns the account holder's name, bank and the last 4 digits of their identity document.

There are two kinds:

ownershipWhat it isProduct neededPasses when
third_partySomeone the party pays.payWith owner_name: the directory reports a match. Without it (a key lookup): the directory resolves the key to a holder.
first_partyThe party's own account.withdrawThe directory reports a match, the holder is the party's verified (KYC) name, and the holder document is the party's. Not available for a party that stands for a payments provider account TuCOP linked to your integrator (409 first_party_not_available).

Every request needs your consumer key (X-TuCOPRamp-Key or Authorization: Bearer) and a party session in the X-TuCOPRamp-Party-Session header. A session with no party linked to your app yet gets 403 party_required. To register a key, the party must have finished identity verification (GET /parties/me), or the request gets 409 party_not_verified, and your consumer needs the product for that kind of destination.

The Bre-B key is never stored. TuCOP keeps a hash of it (to find it again) and the masked key (****4567).

Register a key

bash
curl https://sandbox.api.ramp.tucop.xyz/destinations \
  --request POST \
  --header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
  --header "X-TuCOPRamp-Party-Session: $PARTY_SESSION" \
  --header "Idempotency-Key: 5f0c7a1e-3b8d-4f42-9a6e-1c2d3e4f5a6b" \
  --header "Content-Type: application/json" \
  --data '{ "ownership": "third_party", "bre_b_key": "3001234567", "owner_name": "Carlos Ruiz" }'
  • owner_name is optional for third_party: the holder name as the payer typed it, at least two words. Leave it out to look the key up and get the holder from the directory. For first_party it is ignored and the party's verified name is used.
  • If the party already has this key (the same key as first_party, or the same key added by your consumer as third_party), the existing destination comes back with 200. To pay the same key again, reuse its destination_id (GET /destinations lists them); you do not need to register it again.
  • If the key was registered at the provider outside your app (for the same customer), TuCOP reuses that account and the answer is 202 as usual.
  • Otherwise the answer is 202 with status: "pending".

Key formats

Send the key in the format of its type, as Banco de la República defines it:

TypeFormatExample
Mobile number10 digits, starts with 3, no country code3001234567
Document numberUp to 18 letters or digits, without dots, dashes or spaces1023456789
EmailUp to 30 characters before the @ and 61 after itana@example.com
Alphanumeric key@ followed by 5 to 20 letters or digits, without accents or ñ@anaperez25
Merchant code10 digits starting with 000012345678

How your app gets the key into that format is up to you: add the @ to an alphanumeric key, drop +57, or ask the user which type of key it is. TuCOP checks the key again either way:

  • Bre-B does not tell upper from lower case, so TuCOP writes the key in lower case. @AnaPerez25 and @anaperez25 are the same key and the same destination.
  • A mobile number sent with +57 is taken without it.
  • A key that fits none of the formats gets 400 invalid_bre_b_key at once, without a directory check. TuCOP never adds an @: letters without one are read as a document number.

Register a key when the user submits it, not while they type: every registration is checked against the Bre-B directory.

How many keys

By default there is no limit on how many destinations a party has. A person can register several of their own Bre-B keys (first_party), each with its own withdrawal address; first-party destinations belong to the party and are shared by every app linked to it.

If you want a cap, set it yourself in the integrator console: Maximo de llaves propias por persona on the home page (and on the Sandbox tab for sandbox), from 1 to 1000, or empty for no limit. There is none until you set one. TuCOP may also set a maximum for your integrator; the console shows it (Tope de TuCOP) and does not accept a limit of yours above it. When both are set, the lower one applies, and the error detail says which. It counts only the active first-party keys your app registered for that person: keys another app registered do not count, and neither do deleted or rejected ones. Once the person has that many, registering another first-party key answers 409 destination_limit_reached; delete one of them first. A key the person already has active is not limited: registering it again returns that destination with 200 as usual. Registering a deleted key again counts like a new one. A new limit applies to the next registration; keys already registered stay.

third_party destinations have no count limit either. GET /destinations returns at most 100 of them; reuse the destination_id of a key you already registered instead of registering it again.

json
{
  "id": "dst_01j9z3k8q2m4n6p8r0s2t4v6w8",
  "ownership": "third_party",
  "rail": "bre_b",
  "status": "pending",
  "key_masked": "****4567",
  "owner_name": "Carlos Ruiz",
  "holder": null,
  "rejection_reason": null,
  "verified_at": null,
  "confirmed_at": null,
  "active": true,
  "disabled_at": null,
  "created_at": "2026-10-08T15:01:55.402Z",
  "updated_at": "2026-10-08T15:01:55.402Z"
}

Look up a key

The payer often knows only the Bre-B key. Send ownership: "third_party" and bre_b_key, without owner_name, and TuCOP returns the holder the Bre-B directory has for that key:

bash
curl https://sandbox.api.ramp.tucop.xyz/destinations \
  --request POST \
  --header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
  --header "X-TuCOPRamp-Party-Session: $PARTY_SESSION" \
  --header "Idempotency-Key: 9b2e4c6a-1d3f-4a5b-8c7d-2e4f6a8b0c1d" \
  --header "Content-Type: application/json" \
  --data '{ "ownership": "third_party", "bre_b_key": "3001234567" }'
  1. The answer is 202 with status: "pending" and owner_name: null.
  2. Wait for the directory as usual: the destination.verified webhook, or poll GET /destinations/{id}. It takes about a minute.
  3. Once verified, holder.name, holder.bank and holder.document_last4 say who holds the key. Show them to the payer.
  4. If the payer says it is the right person, confirm the holder. Payments to the destination need it, exactly as for a destination registered with owner_name.

A key lookup is verified when the directory finds a holder for the key. It is rejected with directory_no_match when the directory has no account for the key, with directory_unavailable when the directory fails to answer (also after TuCOP asked again), and with verification_timeout when it does not answer in time. owner_name stays null on the destination; the holder is always in holder.

Sending owner_name is optional. When you send it, it is also checked loosely by the directory, as before: the destination passes only when the directory reports a match. A key you already registered (with or without owner_name) comes back with 200 as it is; it is not looked up again. One exception: when the key was looked up without a name and you now send owner_name, the name is checked against the holder the lookup found, and a name that does not match answers 409 holder_name_mismatch (with destination_id) instead of the destination.

Wait for the directory

The Bre-B directory answers in about a minute (48 to 55 seconds in production). TuCOP does not hold the request open. You learn the result in one of two ways:

  • the destination.verified or destination.rejected webhook, or
  • polling GET /destinations/{id} every 5 to 10 seconds until status is no longer pending. Do not stop at a fixed time of your own: when TuCOP asks the directory again (see below), a destination can stay pending for up to about 10 minutes.

Then store the destination as Keep your copy in sync says.

A verified destination shows what the directory returned:

json
{
  "id": "dst_01j9z3k8q2m4n6p8r0s2t4v6w8",
  "status": "verified",
  "holder": { "name": "CARLOS RUIZ", "bank": "Bancolombia", "document_last4": "4821" },
  "verified_at": "2026-10-08T15:02:51.120Z"
}

A rejected destination has a rejection_reason and no holder data. TuCOP deletes the account at the provider, and the same key can be registered again.

rejection_reasonMeaning
directory_no_matchThe directory has no account for this key, or it does not match the owner_name sent. For a key lookup: no holder was found for the key.
holder_name_mismatchfirst_party only: the holder is not the party.
holder_document_mismatchfirst_party only: the holder document is not the party's.
verification_timeoutThe directory did not answer within 5 minutes. Try again later.
directory_unavailableThe directory failed to answer (for example "Transaction rejected"), and failed again when TuCOP asked a second time. The key may well exist: register it again later.

When the directory's first answer is a passing failure rather than "no account for this key", TuCOP asks it again once, on its own: the destination stays pending for about another minute. Do not retry a directory_no_match yourself; it means the key does not exist. Retry only directory_unavailable and verification_timeout, a few minutes later.

Confirm the holder

Before a payment to a third_party destination, show the payer holder.name, holder.bank and holder.document_last4, and ask them to confirm this is the account they mean to pay. Then record it:

bash
curl https://sandbox.api.ramp.tucop.xyz/destinations/dst_01j9z3k8q2m4n6p8r0s2t4v6w8/confirm \
  --request POST \
  --header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
  --header "X-TuCOPRamp-Party-Session: $PARTY_SESSION" \
  --header "Idempotency-Key: 0e6d1f2a-7c4b-4b9e-8a31-5d2c9f0e7a14"

The answer is the destination with confirmed_at set. A destination that is not verified gets 409 destination_not_verified. Confirming again changes nothing.

When a destination can be paid

StepNeeds
POST /quotesstatus: "verified"
POST /paymentsstatus: "verified" and confirmed_at set, or 409 destination_not_confirmed

There is no confirmed status: a destination is confirmed when confirmed_at is set, and it stays confirmed for every later payment. TuCOP is the source of truth. Before you quote, read the destination with GET /destinations/{id} and use its status, confirmed_at and holder rather than a copy you saved earlier; when they disagree, update your copy from TuCOP's, and never overwrite a stored holder with an empty value.

Keep your copy in sync

If you keep destinations in your own database, update that copy from TuCOP every time it changes. A copy that still says pending after TuCOP verified the key is the most common integration bug.

  • The 202 from POST /destinations is never the final state: it always says pending. Store the destination again once it leaves pending.
  • Store the whole destination every time you read it with GET /destinations/{id}, not only the one from registration. status, holder, verified_at and confirmed_at change after you register the key.
  • With webhooks: on destination.verified or destination.rejected, read GET /destinations/{id} and store what it returns. The event is thin on purpose: it has no holder.
  • Without webhooks: poll GET /destinations/{id} every 5 to 10 seconds until status is verified or rejected, and store that answer. It usually takes about a minute, and up to about 10 minutes when TuCOP asks the directory again. Do not give up earlier and leave the destination pending in your copy.
  • If your process restarts while some destinations are pending in your copy, read each of them again, or read GET /events from the last next_after you stored. The feed has every destination.* event even when you have no webhook endpoints.
  • After the payer confirms the holder, store the destination from the confirm response: it has confirmed_at set.
  • Send your own X-Request-Id (a UUID) on each request, or store the one TuCOP returns. When you ask TuCOP about a request, give that id. See Request ids.

List and delete

  • GET /destinations lists the party's destinations your consumer can see: the ones it created, and the party's first_party destinations whoever created them. Newest first, 100 per page (limit from 1 to 100). The answer is { "data": [...], "has_more": true }: when has_more is true, ask for the next page with starting_after set to the last id you got. Deleted destinations are left out. Add ?include_disabled=true to get them too.
  • If TuCOP linked your own payments provider account, every key your users register is under that one account, so this list holds all of them together. Keep which destination_id belongs to which of your users, and show each user only theirs.
  • GET /destinations/{id} returns a destination also after it was deleted, so you can still show it.
  • DELETE /destinations/{id} deletes one and answers 204. Deleting it again also answers 204. A destination with a payment in progress, or with an active withdrawal address, gets 409 destination_in_use: disable the withdrawal first (DELETE /withdrawals/{id}).

Each destination has status (the directory check: pending, verified, rejected) and, separately, active and disabled_at. A deleted destination keeps its status and has active: false and disabled_at set. It cannot be paid or withdrawn to. To use the key again, register it again with POST /destinations.

TuCOP also deletes the account at the provider, except when the destination ever had a withdrawal address. The provider cannot turn a withdrawal address off: USDC sent to a disabled address is still paid out to its key. The account stays at the provider so those funds still reach the person. Registering such a key again brings the same destination back (200, same destination_id, active: true, still verified), and its withdrawal address can be re-enabled. Any other deleted key registered again gets a new destination_id and goes through the directory check again.

Another consumer's third_party destinations are never visible to you: they get 404 destination_not_found.

Errors

CodeStatusWhen
party_session_required401X-TuCOPRamp-Party-Session is missing, and TuCOP has not linked a payments provider account to your integrator.
party_session_invalid401The session is unknown, expired, revoked or belongs to another consumer.
integrator_not_linked409No session was sent, and your integrator's payments provider account is not linked yet.
party_required403The session has no party linked to your app yet. Call POST /parties.
party_not_linked403The party withdrew its consent to your app while the session was open.
party_restricted403TuCOP restricted the party.
product_not_enabled403Registering a key: your consumer does not have pay (third party) or withdraw (first party).
integrator_customer_mismatch403Registering a third-party key: your integrator pays from its own payments provider account, and the session is for another party.
invalid_body400The body failed validation, for example an empty owner_name or an unknown field. See issues.
invalid_bre_b_key400The key fits none of the Bre-B key formats.
invalid_owner_name400owner_name was sent and is not at least two words. Leave it out to look the key up instead.
party_not_verified409The party has not finished identity verification.
kyc_name_missing409first_party: the verified name is not available yet. Retry later.
first_party_not_available409first_party: The party 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.
holder_name_mismatch409third_party: the key was already looked up for this party and its holder does not match the owner_name sent. Show the payer holder.name from that destination.
destination_conflict409Another request is registering the same key right now. Retry.
destination_limit_reached409first_party: the person already has as many active first-party keys registered by your app as allowed (the lower of the limit you set in the integrator console and TuCOP's maximum for your integrator). Delete one first. See How many keys.
destination_already_registered409The key is already registered on this payments provider account, created outside this app, and could not be matched to exactly one. Nothing was created.
destination_invalid422The provider refused the key. Nothing was created.
destination_not_found404No such destination for this party and consumer.
destination_not_verified409Confirming a destination that is not verified.
destination_in_use409Deleting a destination with a payment in progress or an active withdrawal address. Disable the withdrawal first.
invalid_query400include_disabled is not true or false.