Webhooks
Receive webhooks
Get signed notices on your server when a charge is paid or a payout completes.
1Create an endpoint
- 1In the dashboard, open Webhooks and click New endpoint.
- 2Enter your server's public URL. Use
https://. - 3Pick the events. If you pick none, you get all of them.
- 4Copy the secret (
whsec_…). It's shown only once.
Keep the secret
You need it to verify the signature. If you lose it, rotate it in the dashboard. Rotation is immediate: update your server right away.
Private addresses, localhost and cloud metadata addresses are rejected.
2Receive the event
Each notice is a POST with Content-Type: application/json.
| Field | What it is |
|---|---|
id | The event id. It's the same on every retry. Use it to dedupe. |
type | Event type. See Events. |
createdAt | When it happened, in UTC. It doesn't change between retries. |
merchantId | Your merchant account. |
data | The event's own data. |
Headers
| Header | Value |
|---|---|
TuCapi-Signature | t=<unix>,v1=<hex>. See Signatures. |
TuCapi-Event-Id | Same as the body's id. |
TuCapi-Event-Type | Same as type. |
TuCapi-Delivery-Id | Id of this delivery (event × endpoint). It doesn't change between retries. |
TuCapi-Attempt | Attempt number: 1, 2, … |
TuCapi-Source | tucapi. |
User-Agent | tucapi-webhook/2 (+https://tucapi.com). |
POST /webhooks/tucapi
Headers
POST /webhooks/tucapi HTTP/1.1Content-Type: application/jsonTuCapi-Signature: t=1726000000,v1=5f1a…9cTuCapi-Event-Id: evt_2b1c9d0e4f5a4a6b8c7d9e0f1a2b3c4dTuCapi-Event-Type: order.paidTuCapi-Delivery-Id: …TuCapi-Attempt: 1TuCapi-Source: tucapiUser-Agent: tucapi-webhook/2 (+https://tucapi.com)Body
{ "id": "evt_2b1c9d0e4f5a4a6b8c7d9e0f1a2b3c4d", "type": "order.paid", "createdAt": "2026-09-15T18:50:31Z", "merchantId": "3e630ae9-6ee0-4ee7-b40c-1ea853829377", "data": { "orderId": "c9227f9b-13f4-4e9c-8fe0-4644e62b83cd", "merchantOrderRef": "MO-k7x2m9qz-1a2b3c4d", "amount": "125.00", "currency": "USD", "paymentMethod": "PAYPAL", "netUsdt": "120.33", "externalRef": "5O190127TN364715T", "customerEmail": "ana@example.com", "paidAt": "2026-09-15T18:50:31Z", "source": "payment_link" }}3Verify the signature
Before trusting the event, verify TuCapi-Signature with your secret.
Use the raw body, exactly as received. If you parse it and serialize it again, the signature won't match.
The step-by-step algorithm is in Signatures.
const crypto = require("crypto");function verifyTuCapi(rawBody, header, secret, toleranceSec = 300) { const parts = header.split(",").map((p) => p.trim()); const t = parts.find((p) => p.startsWith("t="))?.slice(2); const v1 = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3)); if (!t || v1.length === 0) throw new Error("invalid header"); if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) { throw new Error("outside tolerance window"); } const expected = crypto .createHmac("sha256", secret) .update(`${t}.`) .update(rawBody) .digest("hex"); const ok = v1.some( (sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex")), ); if (!ok) throw new Error("invalid signature");}4Answer fast
Answer 2xx within 10 seconds. Anything else counts as a failure and is retried.
- If your logic is slow, queue the event and process it separately.
- Dedupe by
id: delivery is at least once.
// Express: use the RAW body, not parsed JSONapp.post( "/webhooks/tucapi", express.raw({ type: "application/json" }), (req, res) => { try { verifyTuCapi(req.body, req.get("TuCapi-Signature"), process.env.TUCAPI_WEBHOOK_SECRET); } catch { return res.sendStatus(400); } const event = JSON.parse(req.body); if (alreadyProcessed(event.id)) return res.sendStatus(200); // dedupe if (event.type === "order.paid") { // order.paid has no charge id; the order does (chargeId). // Queue it and call GET /v1/orders/{orderId} later. queuePayment(event.data.orderId); } res.sendStatus(200); // answer fast; process later },);5Test your endpoint
In the dashboard, click Send test on your endpoint.
You get a webhook.test event signed with your real secret, whatever events you're subscribed to. The result shows up in the delivery log.
webhook.test
JSON
{ "id": "evt_…", "type": "webhook.test", "createdAt": "2026-09-15T18:42:07Z", "merchantId": "3e630ae9-6ee0-4ee7-b40c-1ea853829377", "data": { "subscriptionId": "…", "message": "Prueba de webhook de TuCapi Business" }}Best practices
- Answer
2xxfast and process separately. - Dedupe by
id. - Don't rely on arrival order: a retried event can arrive after a newer one.
- When in doubt, check the current state with
GET /v1/orders/{id}orGET /v1/charges/{id}. - If you suspect a leak, rotate the secret.