Rules for AI agents
Read this before an AI coding agent writes or changes a TuCOP Ramp integration.
This page is for AI coding agents. Follow every rule here. When a rule and your own idea of the API disagree, the rule and the linked page win. Do not guess endpoints, fields or error codes: if a page does not list it, it does not exist.
Which page to read
| Task | Read |
|---|---|
| Any task | API conventions, Errors, Authentication, Services |
| First call, test keys | Quickstart, Sandbox |
| Sign a user in, create or link their party, KYC | Parties and sessions |
| Register a Bre-B key, check the holder | Destinations |
| Pay a third party in COP from USDC | Payments |
| Pay with the API key alone, from your own payments provider account | Pay from your own payments provider account |
| Estimate a price before a quote | Payments |
| Pay out USDC to the user's own account | Withdrawals |
| Receive COP as USDC | Deposits |
| Receive events | Webhooks |
| P2P orders, wallet on-ramp, wallet signatures | Not open to integrators: P2P is available only to TuCOP's own apps for now. |
| Exact request and response schemas | The API reference tab (openapi.yaml) |
Hard rules
- Server side only. Call TuCOP Ramp from a backend. Never put an API key in browser or mobile code.
- Never log or commit an API key. Read it from an environment variable such as
TUCOPRAMP_API_KEY. Keys matchtcr_(live|test)_(sk|rk|pk)_[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{43}[0-9A-Za-z]{6}. Treat party session tokens (tps_...) and webhook secrets (whsec_...) the same way. - Sandbox first. Build and test with a
tcr_test_sk_key againsthttps://sandbox.api.ramp.tucop.xyz. Production ishttps://api.ramp.tucop.xyzand takes onlytcr_live_sk_keys. Never run automated tests against production. - Services must be enabled. Before calling a route, read
GET /consumer:consumer.servicesgroups your services by category (on_off_ramp,p2p). A route your services do not open answers403 product_not_enabledwithrequiredandcategory; do not work around it. A service of one category never opens the routes of the other:depositdoes not open/p2p/onramp/*.payanddepositare requested in the console;withdrawis turned on by TuCOP only. See Services. Idempotency-Keyon every platformPOST(all except/sessions/*). Use a new random value (a UUID) per operation. Reuse it only to retry that same operation with the same body.- Use the paths without a version (
/payments, not/v1/payments). Do not send aTuCOPRamp-Versionheader. - Amounts are objects with a decimal string and an asset:
{ "amount": "200000.00", "asset": "COP" }. Use exactly the asset's decimals (COP 2, USDC 6, COPm 18). Never send JSON numbers or floats, and never parse amounts into a floating-point number. - Ids are public ids with a type prefix (
pty_,dst_,qte_,op_,wdr_,dep_,evt_, ...). Store them as opaque strings. Never build or parse them. - Request bodies are strict. Send only the fields the page or the API reference lists. An
unknown field answers
400 invalid_body. - Branch on
codeonly. Errors areapplication/problem+json. Never match ondetail. - Never retry a
4xxunchanged. Fix the request, and use a newIdempotency-Key. Retry a5xxwith backoff and the same key. After429, or409 idempotency_in_progress, wait forRetry-After. - Verify every webhook over the raw body bytes, before any JSON parsing, with the endpoint's
whsec_secret. Reject a timestamp more than 5 minutes off. Storewebhook-idand skip repeats. Events can arrive out of order and more than once. - Pay in the fixed order: register the destination, wait until it is
verified, show the payer the holder and confirm, quote, create the payment, send exactlydeposit.amount.owner_nameis optional on a third-party destination: without it the key is looked up and, after about a minute,holdersays who holds it. Never confirm a holder the payer has not seen.GET /rates?amount_cop=...may come first to show an estimate; it is never the amount to send. - A quote lasts 2 minutes and pays once. Never reuse one; ask for a new quote.
- Do not hard-code identity documents. Read
GET /document-types. A party never usesTI. - 15 to 30 minutes from deposit to pesos is normal. After the USDC land on-chain, the pesos
usually reach the Bre-B key 15 to 30 minutes later (pay and withdraw). Most of that time a
payment still reads
awaiting_funds(the provider is detecting the deposit, about 15 minutes), thenprocessinguntilcompleted. Neither is a failure: do not retry, do not cancel a payment you already funded and do not send the USDC again; wait for the webhook. Tell the user the window fromsettlement_time(min_minutes,max_minutes) on the payment, the withdrawal, the quote or theGET /ratesestimate, instead of hard-coding it. - Respect the rate limits. All your keys share one budget of 1,000 requests per minute on
the platform API, and
GET /rateshas its own 120 per minute. A429carriesRetry-After: wait that long. More keys do not mean more budget. - A read-only key cannot write. A key created as read-only gets
403 api_key_scope_missingon anyPOST,PATCH,PUTorDELETE, including opening a party session. Use a full-access key for writes, and read-only keys for reporting. - Never treat the
202fromPOST /destinationsas final. It always sayspending. Store the destination again after it leavespending: fromGET /destinations/{id}after thedestination.*webhook, by polling, or fromGET /events. See Keep your copy in sync.
Do
- Check
GET /consumerwith the key before anything else. - Open a party session for each user and send it in
X-TuCOPRamp-Party-Session, unless TuCOP linked your own payments provider account. - Call
DELETE /sessions/currentwhen the user signs out. - Read
GET /limitsbefore you offer an amount to pay. - Use
GET /rates?amount_cop=<whole pesos>to show the payer an estimate in USDC before the quote. What the payer sends is always the payment'sdeposit.amount(2 decimals). It equals the quote'ssource_amount; the estimate is not binding. - On
409 quote_expiredfromPOST /payments, ask for a new quote and pay with it: the quote expired or the rate moved and it no longer pays the destination. - Cancel an unfunded payment with
POST /payments/{id}/cancel. - Send each Bre-B key in the format of its type (see
Key formats): a mobile number without
+57, an alphanumeric key with its@. Check the format in your app; TuCOP checks it again. - Register a key with
POST /destinationsonly when the user submits it, and keep itsdestination_id. Registering the same key again returns the same destination. - Follow payments with
operation.updatedwebhooks, and fall back toGET /payments/{id}orGET /events. Follow withdrawal payouts withwithdraw.drained, orGET /withdrawals/{id}(the list leaves payouts out). - Treat
completedas "the money was sent": the provider paid it out through Bre-B, which is instant. Payments and withdrawal payouts use the same statuses (processing,completed,failed,refunded,canceled). Up to 30 minutes from deposit tocompletedis normal; do not resend. - Show
links.kycfromGET /parties/mewhenever it is notnull, even afterstatus.kycisapproved: the COP enablement can still need the person. - To stop a withdrawal address, call
DELETE /withdrawals/{id}and stop showing the address. USDC sent to it afterwards is still paid out to its Bre-B key.POST /withdrawalswith the same destination re-enables the same address. - Manage webhook endpoints in the console (Avisos), up to 10 per environment.
- Answer
2xxto webhook event types you do not handle, and ignore response fields you do not know. - Send a new
X-Request-Id(a UUID) on each request and log it, or log theX-Request-IdTuCOP returns. Include it (orrequest_idfrom the error body) when you report a problem to TuCOP. See Request ids.
Do not
- Do not call
/p2p/*: P2P is available only to TuCOP's own apps, the console does not let integrators request it, and it is not in the API reference. - Do not use Google sign-in for party sessions: it works only on TuCOP's own sites. Use an email code or a wallet signature.
- Do not expect a collect (business pay-in) product: there is none.
- Do not use
/depositsin the sandbox: the payments provider's sandbox does not create COP deposit accounts. - Do not swap straight into
deposit.address: swap into the payer's wallet, then send exactlydeposit.amountfrom it in one transfer. A swap's output is never exact and its sender is the pool, so the deposit is not credited. - Do not register a key while the user is still typing it: each registration is a Bre-B directory check, and a partial key is someone else's or no one's.
- Do not keep your own "confirmed" state for a destination: it is confirmed when
confirmed_atis set. ReadGET /destinations/{id}before quoting, and never overwrite a stored holder with an empty value (thedestination.*webhooks carry no holder, on purpose). - Do not leave a destination
pendingin your own copy after TuCOP settled it, and do not stop polling apendingdestination after a fixed time of your own: it can take up to about 10 minutes. - Do not register a key again after
directory_no_match: the key does not exist. TuCOP already asks the directory a second time when its answer was a passing failure; onlydirectory_unavailableandverification_timeoutare worth retrying, minutes later. - Do not call
DELETE /payments/{id}: it does not exist. Cancel withPOST /payments/{id}/cancel. - Do not send an amount below the minimum or with cents: pay needs whole pesos, at least the
minimum in
GET /limits. - Do not store or log Bre-B keys, identity documents or holder names longer than the user needs.
- Do not put real people's data in the sandbox.
- Do not poll faster than needed: a destination every few seconds, a payment every 15 to 30 seconds.