Webhooks
Every status change on a charge or payout is sent as an event to the endpoints you register in the dashboard under Webhooks. For each endpoint you choose which statuses to receive and set a signing secret.
Event
POST to your endpointjson
{
"id": "evt_3f9c1a7b2d4e6f8091a2b3c4",
"object": "event",
"type": "charge.paid",
"created_at": "2026-10-31T12:03:12Z",
"data": {
"object": "charge",
"id": "6dcf2aee0d6148e1a12b78db78",
"reference": "order-1042",
"status": "paid",
"amount": 4990,
"currency": "BRL",
"pix": { "end_to_end_id": "E22896431202610311207sjUGQflhcVE" },
"payer": { "name": "ANA SOUZA", "document": "24577481600" },
"created_at": "2026-10-31T12:00:00Z",
"updated_at": "2026-10-31T12:03:11Z",
"paid_at": "2026-10-31T12:03:11Z"
}
}data is a charge or a payout in the same shape as the API, without the fields that only exist on creation (such as pix.qr_code).
Event types
charge.createdCharge created, waiting for payment
charge.pendingPayment seen, being confirmed
charge.paidMoney received
charge.failedCharge failed or expired
charge.refundedCharge returned to the payer
payout.createdPayout queued
payout.processingPayout sent to the bank
payout.paidMoney delivered to the recipient
payout.failedPayout refused; data.failure says why and the amount is back in your balance
payout.returnedThe recipient bank sent the money back
Headers
Armhed-Event-IdstringSame as the body id. Stays the same across retries: use it to ignore duplicates
Armhed-Signaturestringt=<unix seconds>,v1=<hex HMAC-SHA256>. Sent when the endpoint has a signing secret
Armhed-Redeliverystringtrue when the event was resent manually from the dashboard
Verifying the signature
Compute HMAC-SHA256 with your secret over <t>.<raw body>, compare it to v1 in constant time, and reject events whose t is more than five minutes old. Use the raw request body, before any JSON parsing.
Node.js (Express)javascript
import crypto from "node:crypto"
import express from "express"
const app = express()
app.post("/webhooks/armhed", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Armhed-Signature") ?? ""
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")))
const expected = crypto
.createHmac("sha256", process.env.ARMHED_WEBHOOK_SECRET)
.update(`${t}.${req.body}`)
.digest("hex")
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
const valid = v1 && v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
if (!fresh || !valid) return res.sendStatus(400)
const event = JSON.parse(req.body)
// dedupe on event.id, then handle event.type
res.sendStatus(204)
})Delivery and retries
- Reply with any
2xxwithin 15 seconds. Do slow work after replying. - Any other status, a timeout, or a body that says
false,failedorerrorcounts as a failure. - Failed deliveries are retried 1, 5 and 15 minutes later. After that the event stays in the dashboard log, where you can resend it.
- Events can arrive more than once and out of order. Dedupe on
idand, when in doubt, retrieve the charge or payout from the API. - Deliveries come from 144.126.159.43 and 2605:a140:2365:9572::1 if you want to allowlist them.