Skip to content
PortunaPortunaPortunaDocsTestnet

Webhook events

How to verify and handle deliveries, with code: Webhooks.

Each event goes to your webhook URL as a POST with a JSON body. Events are recorded only while a webhook is set.

  • Delivery is at least once: handle each event id once. Order is not guaranteed: rely on the status in data, not on the order of events.
  • Verify every delivery: v1 in Portuna-Signature is the HMAC-SHA256, in hex, of the string <t>.<body> with your webhook secret as the key, where <body> is the raw request body. Accept t only within 5 minutes of your clock. Each attempt is signed anew.
  • Answer with any 2xx within 10 seconds. Any other answer, a redirect included, or no answer in time is a failed attempt: the event is tried again after 10 s, 20 s, 40 s and so on, up to an hour between attempts, 12 attempts in all.
  • New event types may come: accept and ignore the ones you do not handle.
NameTypeDescription
Portuna-Signature requiredstringt=<unix seconds>,v1=<signature>: the time of this attempt and the HMAC-SHA256 of <t>.<body> with your webhook secret, in hex. Example: t=1791634982,v1=6e735f32a0…
Portuna-Event requiredstringThe event type, as in type in the body. Example: sweep.swept
Portuna-Delivery requiredstring (uuid)The event’s id, as in the body. It is the same in every attempt.
User-AgentstringNames the sender. Verify deliveries by their signature, not by this header. Example: portuna-webhooks/1

The body is an Event object.

The body of a webhook delivery. type tells what happened and what data is: a sweep as in GET /v1/sweeps/{id}, a revocation as in GET /v1/revocations/{id}, a hot wallet as in GET /v1/hot-wallets, a low balance alert or a test event. Sweeps, revocations and hot wallets are shown as they are when the event is delivered.

FieldTypeDescription
id requiredstring (uuid)The event’s id, the same in every attempt.
type requiredstringThe event type. One of: sweep.swept, sweep.failed, sweep.rejected, revocation.revoked, revocation.failed, revocation.rejected, hot_wallet.active, hot_wallet.failed, balance.low, webhook.test.
created_at requiredstring (date-time)When the event happened.
data requiredSweep | Revocation | HotWallet | BalanceLow | WebhookTestDepends on type: see below.
TypedataDescription
sweep.swept
sweep.failed
sweep.rejected
SweepA sweep ended. sweep.swept: it moved the funds; sweep.failed: it reverted on chain or ran out of attempts; sweep.rejected: it was never sent.
revocation.revoked
revocation.failed
revocation.rejected
RevocationA revocation ended. revocation.revoked: the delegation is removed; revocation.failed: the chain skipped the authorization, or the attempts ran out; revocation.rejected: it was never sent.
hot_wallet.active
hot_wallet.failed
HotWalletA hot wallet’s delegate deployment ended. hot_wallet.active: the delegate is deployed; hot_wallet.failed: the deployment failed.
balance.lowBalanceLowYour gas wallet fell below a low balance threshold. One event each time it falls below; the alert re-arms once the balance recovers.
webhook.testWebhookTestThe test event you asked for with POST /v1/webhook/test.
Content-Type: application/json
User-Agent: portuna-webhooks/1
Portuna-Event: sweep.swept
Portuna-Delivery: 9c41e7d2-58a3-4b0f-a6d1-2e7f3c9b8a05
Portuna-Signature: t=1791634982,v1=<hex HMAC-SHA256>
{
"id": "9c41e7d2-58a3-4b0f-a6d1-2e7f3c9b8a05",
"type": "sweep.swept",
"created_at": "2026-10-11T07:03:02Z",
"data": {
"id": "8a6e2f31-0c4b-4d7a-b1e5-92f3c6d8a047",
"external_id": "dep-80c2d4",
"chain_id": 11155111,
"hot_wallet": "0x2Cd50979a8A33e8CA37DA85F8517fb148a67b769",
"account": "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23",
"token": "USDT",
"token_address": "0x5B74f75040584b133Ff1EB67eEe646B0da26e39C",
"status": "swept",
"attempts": 1,
"swept_amount": "250000000",
"tx_hash": "0xe2a3d8ffdcd7c4ed8a2389a59ce91622863c495d052a7571206bb494919dc35b",
"kind": "repeat",
"gas": 18078,
"gas_cost": "21693600000000",
"fee": "23358000000000",
"classic_gas": 53633,
"rate_bps": 7000,
"created_at": "2026-10-11T07:02:15Z",
"updated_at": "2026-10-11T07:03:02Z"
}
}