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.
Registering your URL
Section titled “Registering your URL”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.
Verifying the signature
Section titled “Verifying the signature”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:
- Read the raw body, before parsing the JSON: re-serializing changes the bytes and the signature no longer matches.
- Reject it if
X-Timestampis more than 5 minutes away from your clock: without this, a legitimate notification captured once works forever. - 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). - 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.
Testing your URL
Section titled “Testing your URL”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.
Delivery and retries
Section titled “Delivery and retries”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.
Querying events
Section titled “Querying events”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.
Routes
Section titled “Routes”Your operations’ events
Section titled “Your operations’ events”/v2/eventsScopeoperaciones: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. |
Look up any operation
Section titled “Look up any operation”/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. |
Register the notification URL
Section titled “Register the notification URL”/v2/webhook-endpointsScopewebhooks: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.
How an event arrives
Section titled “How an event arrives”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. |
The event
Section titled “The event”| Field | Type | Required | Description |
|---|---|---|---|
data |
Operacion |
yes | |
id |
string |
yes | |
occurred_at |
string |
yes | |
type |
TipoDeEvento |
yes |
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" }'