Payouts

Webhooks

Muny sends signed HTTPS POST requests to your endpoints when payouts move through their lifecycle and when wallets receive deposits.

Events#

EventSent when
payout.createdA payout is created (including each payout of a batch).
payout.processingA transaction for the payout has been submitted to Solana.
payout.confirmedThe transaction is confirmed on-chain. The payout is complete.
payout.failedSettlement failed. The payload includes failure_code and failure_message.
payout_batch.completedEvery payout in a batch reached a final state.
wallet.deposit_detectedMUNY 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" }
    }
  }
}
HeaderValue
Muny-Signaturet=1790847004,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Muny-Event-IdThe event id. Use it to de-duplicate.
Muny-Event-TypeThe 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-Id and read the current state from the payload's status.
  • 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 with GET /v1/payouts/:id before releasing goods or crediting balances.