Skip to content

Webhooks and events

Every operation that reaches a final state emits one event: payin.confirmed, payin.failed, payout.confirmed or payout.failed (and test.ping is the test notification, below). The event carries the whole operation in data. We send it to your URL by webhook, and you can also query it with GET /v2/events.

POST /v2/webhook-endpoints with {"url": "https://…"} returns the secret only once: store it. One active URL per company: registering another replaces it, and the previous secret stays valid for 24 hours so you can update your copy without losing any notification.

Every notification arrives with these headers:

Header What it carries
X-Firma the signature, v1=<hex>; during a rotation there are two, separated by a space
X-Timestamp Unix seconds of when it was signed; rejected outside the window
X-Evento the event type
X-Id-Evento the event id, for deduplication

Algorithm: HMAC-SHA256 over <X-Timestamp> + "." + <raw body>, in hex; clock tolerance: 300 seconds.

The signature is HMAC-SHA256 over X-Timestamp + "." + raw body, in hexadecimal, keyed with your secret, prefixed with v1=. To verify it:

  1. Read the raw body, before parsing the JSON: re-serializing changes the bytes and the signature no longer matches.
  2. Reject it if X-Timestamp is more than 5 minutes away from your clock: without this, a legitimate notification captured once works forever.
  3. Compute the HMAC and compare it in constant time with each signature in X-Firma (during a secret rotation there are two, separated by a space; it is enough for one to match).
  4. Only then parse the JSON and process the event. Deduplicate by id.

All three SDKs do this for you (ParseEvent, parseEvent, parse_event). It is the part of the SDK most worth using: a badly verified signature is the bug nobody notices.

From the dashboard (Webhooks → Enviar aviso de prueba) we send you a test.ping notification signed exactly like the real ones, with the same headers and the current secret. It lets you check that your URL answers and that your code verifies the signature before the first payment arrives.

Its data is a sample operation: an all-zeros id, status pending, amount 0.00 and metadata.test = true. It does not correspond to any movement. Always decide by type: answer 2xx to test.ping and don’t process it. It is not retried and does not appear in GET /v2/events.

Answer 2xx within 10 seconds. Otherwise we retry with growing backoff for about 24 hours (30 s, 2, 10, 30 min, 1, 2, 4, 8 and 8 h) and only then is the notification considered exhausted; an exhausted notification can be resent on request. The same event may reach you more than once: that is what the id is for.

GET /v2/events?cursor=&limit= returns your events in order with a cursor: use it to reconcile, to recover what you didn’t receive and to integrate without a webhook. Store next_cursor.

GET/v2/events

Scopeoperaciones:leer

The same event that travels by webhook, in order, with a cursor. Use it to recover from a lost webhook: store the last next_cursor and ask from there. Requires the operaciones:leer scope.

Parameter In Required Description
cursor query no The next_cursor from the previous response. Without it, from the beginning.
limit query no
Code Meaning
200 The events.
400 An invalid parameter. Codes: field_invalid.
401 The key is missing or invalid. Codes: unauthorized.
403 The key is valid but lacks the scope. Codes: scope_missing.
GET/v2/transactions/{id}

Scopeoperaciones:leer

A pay-in or a payout by its id, with the same shape. Requires the operaciones:leer scope.

Parameter In Required Description
id path yes The operation id.
Code Meaning
200 The operation.
401 The key is missing or invalid. Codes: unauthorized.
403 The key is valid but lacks the scope. Codes: scope_missing.
404 There is no operation with that id in your company. Codes: not_found.
POST/v2/webhook-endpoints

Scopewebhooks:configurar

Registers the URL events are sent to and returns the signing secret only once. One active URL per company: registering another replaces it, and the previous secret stays valid for 24 hours so the switch loses no notifications.

Requires the webhooks:configurar scope. The URL must be https.

A POST to your URL with the Evento body and these headers: X-Firma (HMAC-SHA256 of <X-Timestamp>.<body> with your secret, as v1=<hex>; during a rotation there are two, separated by a space), X-Timestamp (Unix seconds), X-Evento (the type) and X-Id-Evento (the id). Answer 2xx within 10 seconds; otherwise it is retried with growing backoff for about 24 hours (30 s, 2, 10, 30 min, 1, 2, 4, 8 and 8 h, with ±10 % jitter) and only then is it exhausted; an exhausted notification is resent manually. Deduplicate by id.

Until you register something here, events go to the URL and with the secret of version 1, if you have them.

Code Meaning
201 Registered. Store secret: it is not shown again.
400 The URL is missing or is not https. Codes: body_invalid, field_invalid, field_required.
401 The key is missing or invalid. Codes: unauthorized.
403 The key is valid but lacks the scope. Codes: scope_missing.
Field Type Required Description
data Operacion yes
id string yes
occurred_at string yes
type TipoDeEvento yes
POST /v2/webhook-endpoints
curl -X POST https://api.tucapi.app/v2/webhook-endpoints \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourcompany.com/notifications"
}'