Payouts
Webhooks
Muny sends signed HTTPS POST requests to your endpoints when payouts move through their lifecycle and when wallets receive deposits.
Events#
| Event | Sent when |
|---|---|
payout.created | A payout is created (including each payout of a batch). |
payout.processing | A transaction for the payout has been submitted to Solana. |
payout.confirmed | The transaction is confirmed on-chain. The payout is complete. |
payout.failed | Settlement failed. The payload includes failure_code and failure_message. |
payout_batch.completed | Every payout in a batch reached a final state. |
wallet.deposit_detected | MUNY arrived in one of your wallets. |
Creating an endpoint#
Add endpoints in Developers → Webhooks or over the API. Endpoint URLs must use HTTPS. The signing secret (whsec_…) is shown once; rotate it any time.
ts
const endpoint = await muny.webhooks.create({
url: "https://example.com/webhooks/muny",
events: ["payout.confirmed", "payout.failed", "payout_batch.completed"],
});
// Store endpoint.secret securely — it is only returned now.Payload#
POST https://example.com/webhooks/muny
{
"id": "evt_0b8f6c3e-2a5d-4f19-8e7a-6c2d1b0a9f34",
"object": "event",
"type": "payout.confirmed",
"created_at": "2026-10-01T09:30:04.112Z",
"organization_id": "3f6a2c71-8d4e-4b0a-9c55-1e2f3a4b5c6d",
"livemode": false,
"data": {
"object": {
"id": "6c1f0a2e-93b4-4c8e-9a51-3f0d7e2b8c41",
"object": "payout",
"status": "confirmed",
"amount": "500",
"token": "MUNY",
"reference": "AFFILIATE-8392",
"transaction": { "signature": "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnb…", "status": "confirmed" }
}
}
}| Header | Value |
|---|---|
Muny-Signature | t=1790847004,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd |
Muny-Event-Id | The event id. Use it to de-duplicate. |
Muny-Event-Type | The event type, e.g. payout.confirmed. |
Verifying signatures#
The signature is hex(HMAC-SHA256(secret, "{t}.{rawBody}")), where t is the Unix timestamp from the header. Always verify against the raw request body, before parsing JSON, and reject timestamps more than 300 seconds old to block replays.
import { verifyWebhook } from "@muny/sdk";
app.post("/webhooks/muny", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await verifyWebhook(
req.body.toString("utf8"),
req.header("muny-signature"),
process.env.MUNY_WEBHOOK_SECRET!,
);
await queue.add(event.type, event); // do the work asynchronously
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
});Delivery and retries#
- Respond with any 2xx status within 10 seconds. Do slow work after responding.
- Non-2xx responses, timeouts and connection errors are retried with exponential backoff — up to 8 attempts over roughly a day.
- Deliveries can arrive more than once or out of order. De-duplicate on
Muny-Event-Idand read the current state from the payload'sstatus. - Every attempt is logged with its response code, body excerpt and duration under the endpoint in the dashboard.
Resending
Resend any delivery from the dashboard or with POST /v1/webhook-deliveries/:id/resend. Send a test event with POST /v1/webhooks/:id/test.
Treat webhooks as a signal
For high-value flows, confirm a payout's state withGET /v1/payouts/:id before releasing goods or crediting balances.