Skip to main content

Overview

Dancity sends an HTTP POST request to your configured webhook URL whenever a transaction event occurs. This allows you to keep your system in sync without polling the transactions endpoint.

Setting up your webhook URL

  1. Go to your API Key page
  2. Enter your webhook URL under Webhook URL — it must be HTTPS
  3. Click Save Webhook URL
Your webhook URL must be publicly accessible. Localhost URLs will not work in production. Use a tool like ngrok during local development.

Event payload

All webhook payloads share the same envelope structure:
{
  "event": "transaction.success",
  "timestamp": "2024-04-21T10:30:00.000Z",
  "data": {
    "transactionId": "TXN-2024-XXXXX",
    "type": "AIRTIME",
    "status": "success",
    "amount": 500,
    "currency": "NGN",
    "phone": "08012345678",
    "reference": "dcy_ref_xxxxxxxx",
    "createdAt": "2024-04-21T10:29:58.000Z"
  }
}

Event types

EventDescription
transaction.successA VAS purchase or transfer completed successfully
transaction.failedA transaction failed
transaction.pendingTransaction is awaiting confirmation
wallet.fundedYour wallet was funded
wallet.debitedYour wallet was debited

Verifying webhook signatures

Every webhook request includes an X-Dancity-Signature header. You must verify this signature to confirm the request is genuinely from Dancity and has not been tampered with. The signature is computed as:
HMAC-SHA256(webhookSecret, rawRequestBody)
Your webhookSecret is shown when you first generate your API key. Store it securely — it is not shown again.

Verification examples

const crypto = require("crypto");

app.post("/webhooks/dancity", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-dancity-signature"];
  const secret = process.env.DANCITY_WEBHOOK_SECRET;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(req.body) // raw Buffer — do NOT parse JSON before this
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body);
  console.log("Received event:", event.event);

  // Handle the event
  if (event.event === "transaction.success") {
    // fulfil your order, credit your user, etc.
  }

  res.sendStatus(200);
});
Always use a timing-safe comparison function (e.g. crypto.timingSafeEqual, hmac.compare_digest, hash_equals) to prevent timing attacks.

Retry policy

If your endpoint returns any HTTP status other than 2xx, Dancity will retry the delivery with exponential backoff:
AttemptDelay
1st retry30 seconds
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry8 hours
After 5 failed attempts, the event is marked as undeliverable.

Best practices

Return a 200 response as fast as possible — ideally within 5 seconds. Move any heavy processing (database writes, emails) to an async job/queue.
You may receive the same event more than once due to retries. Use transactionId or reference as a unique key to ensure you don’t process the same transaction twice.
Save the raw payload and the X-Dancity-Signature header for every incoming webhook. This makes debugging failed deliveries much easier.
Run ngrok http 3000 to get a public HTTPS URL that tunnels to your local server. Set that URL as your webhook URL during development.