Parties and sessions
Open a party session for your user, create or link their party, and follow identity verification.
A party is a real person (individual) or company (business) that TuCOP serves. There is one
party per identity document, shared by every app that serves the same person or company. Your app reaches a party
through a party session: proof that the person using your app controls an email or a wallet.
Every request below needs your consumer key (X-TuCOPRamp-Key or Authorization: Bearer). Requests
to /parties, and to /destinations, /quotes,
/payments, /limits, /withdrawals and
/deposits, also need the party session in the X-TuCOPRamp-Party-Session header.
The one exception is an integrator whose own payments provider account TuCOP linked: see
Pay from your own payments provider account. POST /parties always needs a
session.
Onboarding flow
Open a party session
Prove the person's email with a code, or their wallet with a signature. You get a session token and, when the person already has a party linked to your app, its
party_id.Create or link the party
POST /partieswith the party type, the country, the legal name, the identity document and the consent your app collected. A new person or company gets a party and identity verification links (201). A person TuCOP already knows, who proved an email or wallet attached to their party, is linked to your app (200).Send the person through verification
Open
party.links.kycandparty.links.tosfor the person. Both are hosted pages. When they are done, the person returns toredirect_url.Wait for approval
Listen for
party.updated, or pollGET /parties/me. The party can use products oncestatus.kycisapproved,status.tosisacceptedand thecopendorsement isapproved. Iflinks.kycis set again after KYC is approved, the COP enablement still needs something from the person: show it to them again.Use a product
Register the Bre-B keys the person pays with Destinations, then quote and pay with Payments.
Party sessions
A party session:
- belongs to your consumer. Another consumer's key is refused with it (
401 party_session_invalid); - expires one hour after it is issued. Open a new one when it expires;
- is shown once. TuCOP stores only a hash of it.
The session endpoints exchange a one-time proof for a session, so they do not take an
Idempotency-Key (a stored response would hold the session token). If a call fails, start again
with a new code or nonce.
With an email code
curl https://sandbox.api.ramp.tucop.xyz/sessions/email-otp \
--request POST \
--header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
--header "Content-Type: application/json" \
--data '{ "email": "ana@example.com" }'
TuCOP emails a 6-digit code, valid 10 minutes. The answer is 202 with when the code expires, the
same whether or not the email is known:
{ "expires_at": "2026-10-08T15:12:00.000Z" }
Then:
curl https://sandbox.api.ramp.tucop.xyz/sessions/email-otp/verify \
--request POST \
--header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
--header "Content-Type: application/json" \
--data '{ "email": "ana@example.com", "code": "493027" }'
{
"token": "tps_q1Xn0d3S2m6v9bK8aP4tZr7wYc5eH1jL0uG2fN3sD8o",
"method": "email_otp",
"expires_at": "2026-10-08T16:05:00.000Z",
"party_id": null
}
- Only the newest code sent through your consumer counts. A code sent through another app does not.
- Five wrong tries spend a code. Three codes per email every 15 minutes, and ten verify attempts per email every 15 minutes.
- The verify call answers
201with the session.
With a wallet signature
Get the message
POST /sessions/wallet/noncewith{ "address": "0x..." }. You may also send"chain_id": 42220; it is the default, and any other chain gets422 chain_not_supported. The answer (201) hasnonce,message,chain_idandexpires_at. The message names your consumer, the wallet, the chain (Celo,42220), the nonce and its expiry (5 minutes).Sign it
Sign
messageexactly as returned with EIP-191 (personal_sign). Only EOA signatures are accepted.Exchange it
POST /sessions/walletwith{ "address", "nonce", "signature" }. A nonce works once, and only for the consumer it was issued to.
{
"nonce": "9f2c4e6a8b0d1f3a5c7e9b1d3f5a7c9e",
"message": "TuCOPRamp party session\nConsumer: con_0bdy554f0x9szsmatwdnz8z6gb\nWallet: 0x1111111111111111111111111111111111111111\nChain ID: 42220\nNonce: 9f2c4e6a8b0d1f3a5c7e9b1d3f5a7c9e\nExpires: 2026-10-08T15:07:00.000Z",
"chain_id": 42220,
"expires_at": "2026-10-08T15:07:00.000Z"
}
At most 10 nonces per wallet address per minute (429 rate_limited_wallet).
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.USER_PRIVATE_KEY);
const { nonce, message } = await api.post('/sessions/wallet/nonce', { address: account.address });
const signature = await account.signMessage({ message });
const session = await api.post('/sessions/wallet', { address: account.address, nonce, signature });
With Google (TuCOP sites only)
TuCOP's own site also signs people in with Google (POST /sessions/google/nonce, then
POST /sessions/google with the ID token). Google only runs TuCOP's sign-in on pages TuCOP
registered, so it does not work from your app: use the email code or a wallet signature. A Google
session proves the account's email, the same as an email code ("method": "google"), so a person
who signed up with a code and later uses Google with the same email reaches the same party.
Using the session
curl https://sandbox.api.ramp.tucop.xyz/parties/me \
--header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
--header "X-TuCOPRamp-Party-Session: $PARTY_SESSION"
A session acts for the party that holds its email or wallet, once that party is linked to your app.
Until then, endpoints that need a party answer 403 party_required: call POST /parties.
Signing out
DELETE /sessions/current with the session in X-TuCOPRamp-Party-Session revokes it at once:
later calls with it get 401 party_session_invalid. Call it whenever your user signs out, instead of
only forgetting the token on your side. It answers 204 whether or not there was a live session.
Without the header it answers 401 party_session_required.
Create or link a party
curl https://sandbox.api.ramp.tucop.xyz/parties \
--request POST \
--header "X-TuCOPRamp-Key: $TUCOPRAMP_API_KEY" \
--header "X-TuCOPRamp-Party-Session: $PARTY_SESSION" \
--header "Idempotency-Key: 5a0e3c2d-8f4b-4f60-9a1d-2c7e6b9f0a13" \
--header "Content-Type: application/json" \
--data '{
"type": "individual",
"country": "COL",
"first_names": "Ana Maria",
"last_names": "Perez Gomez",
"nationality": "COL",
"document_type": "CC",
"document_number": "1017123456",
"consent": { "version": "2026-10-08", "accepted_at": "2026-10-08T15:02:11Z" },
"redirect_url": "https://yourapp.example/kyc/done"
}'
{
"party": {
"id": "pty_01j9z3k8q2m4n6p8r0s2t4v6w8",
"type": "individual",
"country": "COL",
"status": {
"kyc": "not_started",
"tos": "pending",
"endorsements": [{ "name": "cop", "status": "pending" }]
},
"restricted": false,
"links": {
"kyc": "https://verify.example/kyc/...",
"tos": "https://verify.example/tos/..."
},
"created_at": "2026-10-08T15:02:12.418Z",
"updated_at": "2026-10-08T15:02:12.418Z"
}
}
| Field | Notes |
|---|---|
type | individual or business. A person is verified with hosted KYC, a company with hosted KYB. |
country | ISO 3166-1 alpha-3 code of the country that issued the document. Optional, defaults to COL. |
first_names, last_names | Individuals: given names and surnames, as on the document. Send both. Preferred over legal_name. |
nationality | Individuals: ISO 3166-1 alpha-3. Optional. It can differ from country: a foreigner with a Colombian cédula de extranjería has nationality VEN and country COL. An unknown code answers 400 invalid_nationality. |
legal_name | A business: its registered name (required). An individual: the full legal name, at least two words, only when first_names and last_names are not sent. |
document_type, document_number | A type GET /document-types lists for country and type. Dots, spaces and dashes are ignored: 1.017.123.456 and 1017123456 are the same document. |
consent | The version of the consent text the person accepted in your app, and when. Recorded per app. |
email | Only for sessions opened with a wallet: the contact email for identity verification. It is not attached to the party as a proven email. Without it, a wallet session gets 422 email_required when the party has no verified email. |
redirect_url | Where verification sends the person back. Must be one of the return URLs TuCOP has on file for your app. Optional. |
The answer:
201: a new party. TuCOP created its verification account and returned the links.200: the party already existed and the session proved an email or wallet attached to it. It is now linked to your app. If it already had a verification account, no new one is created; its status may already beapproved.
Documents per country
GET /document-types?country=COL&party_type=individual lists the identity documents a party may
give, so your form does not hard-code them. country is an ISO 3166-1 alpha-3 code, required.
party_type is individual (the default) or business. It needs only your consumer key.
{
"country": "COL",
"country_name": "Colombia",
"party_type": "individual",
"document_types": [
{ "code": "CC", "name": "Cedula de ciudadania", "validation": "local", "hint": "1 a 10 digitos" },
{ "code": "CE", "name": "Cedula de extranjeria", "validation": "local", "hint": "1 a 10 digitos" },
{ "code": "NUIP", "name": "Numero unico de identificacion personal", "validation": "local", "hint": "10 digitos, desde 1000000000" },
{ "code": "PAS", "name": "Pasaporte", "validation": "local", "hint": "5 a 20 letras y numeros" }
]
}
A missing or unknown country gets 400 invalid_country, and a party_type other than
individual or business gets 400 invalid_party_type. The answer may be cached for an hour.
- Colombia answers TuCOP's own types (
CC,CE,NUIP,PASfor individuals,NITfor businesses) withvalidation: "local": the number is checked with the issuer's rule, including the NIT check digit. TI(tarjeta de identidad) is not accepted for a party: it is a minor's document, and identity verification is for adults only.- Other countries answer the list the payments provider publishes for that country, plus the types it accepts
for any non-US country. Where the issuer's format is published (for example Venezuela's RIF,
Ecuador's cedula, Peru's DNI, Chile's RUT, Argentina's CUIL and DNI, Mexico's CURP, Brazil's CPF,
Spain's NIF and NIE, the US SSN), the type has
validation: "local"and the number is checked for length, format and check digit. The rest havevalidation: "format_only": only letters and digits within a length are checked. Passports are alwaysformat_only(5 to 20 letters or digits). - Each type has a
hintdescribing the expected number. Show it next to the field.
POST /parties accepts the same types for the same country and type. Any other combination
answers 400 invalid_document with reason: "type_not_accepted", for example a CC for a
business or a NIT for an individual. For Colombia (country COL or left out), a
document_type that is not a Colombian type at all (not CC, CE, TI, NUIP, NIT or PAS)
answers 400 invalid_body instead, with document_type in issues.
- A Colombian NIT may carry its check digit after a dash (
900123456-8). When it does, the digit must be right, or the answer is400 invalid_documentwithreason: "bad_verification_digit". The digit is not part of the NIT:900123456-8and900123456are the same company. - Every
invalid_documentanswer has areasonfield next tocode, to branch on (never ondetail):type_not_accepted,unknown_country,not_numeric,not_alphanumeric,too_short,too_long,leading_zero,bad_length,bad_formatorbad_verification_digit. - A party from outside Colombia is verified by the payments provider's hosted KYC or KYB like any other party. Its number is checked with the rule its type lists. The same number from two countries is two different parties.
Businesses
A business party is created the same way, with type: "business" and a business document. Its
links.kyc opens a hosted KYB: the person filling it in gives the company's details and its
beneficial owners. While the owners are pending, status.kyc is incomplete.
{
"type": "business",
"country": "COL",
"legal_name": "Empresa Ejemplo SAS",
"document_type": "NIT",
"document_number": "900123456-8",
"consent": { "version": "2026-10-08", "accepted_at": "2026-10-08T15:02:11Z" }
}
Identity must be proven
TuCOP links an existing party to your app only when the session's email, or the session's wallet,
is already attached to that party. Knowing the document number is not enough. When the session
cannot prove the identity, the answer is 409 party_identity_not_proven:
- the document belongs to a party the session's email or wallet is not attached to, or
- the session's email or wallet already belongs to a party with a different document.
The answer is the same in both cases and never says which party holds the document. Each one is
logged and reviewed by TuCOP. Ask the person to sign in with the email or wallet they used before,
or contact TuCOP support with the request_id.
Wallets linked to a document in the P2P flow before party sessions existed were attached without a signature. They do not prove a party on their own.
Status
GET /parties/me returns the party, its status and the instruments your app may see. While
verification is moving, TuCOP re-reads it from the provider on this call (at most every 10 seconds).
{
"party": {
"id": "pty_01j9z3k8q2m4n6p8r0s2t4v6w8",
"type": "individual",
"country": "COL",
"status": {
"kyc": "approved",
"tos": "accepted",
"endorsements": [{ "name": "cop", "status": "approved" }]
},
"restricted": false,
"links": { "kyc": null, "tos": null },
"created_at": "2026-10-08T15:02:12.418Z",
"updated_at": "2026-10-08T15:20:40.512Z"
},
"instruments": {
"destinations": [
{
"id": "dst_01j9z3k8q2m4n6p8r0s2t4v6w8",
"ownership": "third_party",
"status": "verified",
"key_masked": "****4567",
"bank": "Bancolombia",
"created_at": "2026-10-08T15:01:55.402Z"
}
]
}
}
| Field | Values |
|---|---|
status.kyc | not_started, incomplete (the person has to act in links.kyc), under_review, approved, rejected, suspended |
status.tos | pending, accepted |
status.endorsements[] | { "name": "cop", "status": "pending" | "approved" | "revoked" }. cop clears the party for Colombian pesos over Bre-B. |
restricted | Set by TuCOP. A restricted party gets no instruments. |
links.kyc, links.tos | Hosted pages. null once there is nothing left to do. links.kyc can come back after status.kyc is approved (see below). |
instruments.destinations | Destinations your app created for the party, and the party's own (first-party) destinations, which are shared between apps. Oldest first, without deleted ones. Empty while the party is restricted. Read the full destination with GET /destinations/{id}. |
When links.kyc comes back after KYC is approved
Sometimes status.kyc is approved and status.tos is accepted, but the cop endorsement
stays pending because the COP enablement still needs something from the person (for example,
a person who already had an account with the payments provider). TuCOP then
sets links.kyc again, to a hosted page where the person completes that step, while links.tos
stays null.
Whenever links.kyc is not null, show it to the person, even if you already took them through
KYC. Once the cop endorsement is approved, links.kyc goes back to null. The link is
refreshed by TuCOP, so always use the one from the latest response rather than one you stored.
Webhooks
party.updated is sent to every app linked to the party when its verification status, its
endorsements or its restriction change. data:
{
"party_id": "pty_01j9z3k8q2m4n6p8r0s2t4v6w8",
"status": {
"kyc": "approved",
"tos": "accepted",
"endorsements": [{ "name": "cop", "status": "approved" }]
},
"restricted": false,
"updated_at": "2026-10-08T15:20:40.512Z"
}
Events can arrive out of order. Keep the one with the latest updated_at, or read
GET /parties/me. See Webhooks.
Error codes
| Code | Status | When |
|---|---|---|
invalid_code | 401 | The email code is wrong, used or expired. |
nonce_invalid | 401 | The wallet or Google nonce is unknown, used, expired or was issued to another consumer. |
signature_invalid | 401 | The signature does not match the message for that wallet. |
chain_not_supported | 422 | A wallet nonce for a chain other than Celo (42220). |
google_sign_in_unavailable | 503 | Google sign-in is not set up on this deployment. It works only on TuCOP's own sites. |
id_token_invalid | 401 | The Google ID token is not valid. |
email_not_verified | 401 | Google has not verified the account's email. |
party_session_required | 401 | X-TuCOPRamp-Party-Session is missing, and TuCOP has not linked a payments provider account to your integrator. POST /parties always needs a session, even then. |
party_session_invalid | 401 | The session is unknown, expired, revoked or belongs to another consumer. |
party_required | 403 | The session has no party linked to your app yet. Call POST /parties. |
invalid_country | 400 | country is not an ISO 3166-1 alpha-3 code (POST /parties, GET /document-types). |
invalid_party_type | 400 | GET /document-types: party_type is not individual or business. |
invalid_body | 400 | The body failed validation, for example a missing field or, for Colombia, an unknown document_type. See issues. |
invalid_document | 400 | The document type is not accepted for the country and party type, or the number does not fit its type. |
invalid_name | 400 | An individual's legal name is not at least two words, or a business name is empty. |
invalid_nationality | 400 | nationality is not an ISO 3166-1 alpha-3 code. |
invalid_consent | 400 | consent.accepted_at is in the future. |
email_required | 422 | A wallet session creating a party with no email to send verification to. |
redirect_url_not_allowed | 422 | redirect_url is not one of your return URLs. |
party_identity_not_proven | 409 | The session cannot prove the identity that holds the document. See above. |
provider_unavailable | 502 | The verification provider failed. Retry with the same Idempotency-Key. |
email_not_sent | 502 | The email with the code could not be sent. Retry later. |
rate_limited_email | 429 | Too many codes or attempts for this email. See Retry-After. |
rate_limited_wallet | 429 | More than 10 wallet nonces for this address in a minute. See Retry-After. |
rate_limited_ip | 429 | Your consumer is a trusted relay and one end user's address went over its session limits. See Retry-After. |
rate_limited_party_session | 429 | Too many POST /parties with this session (10 per 15 minutes). See Retry-After. |
rate_limited_consumer | 429 | Too many session requests for your whole consumer. See Retry-After. |