Webhooks

Webhooks allow Tulu Switch to push real-time event notifications to your server as things happen — a deposit confirmation, a failed transfer, a subscription charge. Instead of polling the API, your application receives an HTTP POST to a URL you control.

You configure one webhook URL per environment (TEST and LIVE are separate). Each environment also gets its own signing secret for verifying that payloads are genuine.

Registering a Webhook

Webhook configuration is done entirely from your dashboard — there is no API endpoint for this. To register a webhook URL:

  1. Log in to your Tulu Switch dashboard.
  2. Navigate to Account > Webhooks in the sidebar.
  3. Enter your HTTPS endpoint URL and save.
  4. Copy the generated signing secret immediately — it will not be shown again.

TEST and LIVE webhook URLs are configured separately. Switch between environments in the dashboard to configure each one.

Only HTTPS endpoints are accepted. Save the signing secret as soon as it appears — you cannot retrieve it afterwards, only rotate it.

Webhook Secret

The webhook secret is a whsec_-prefixed 64-character hex string. It is used to compute the HMAC-SHA256 signature sent with every event. Store it in an environment variable and never expose it in client-side code.

You can rotate the secret at any time from Account > Webhooks in the dashboard. The old secret becomes invalid immediately on rotation.

Delivery Headers

Every webhook POST to your URL includes these headers:

HeaderValue
Content-Typeapplication/json
X-Tulu-Switch-SignatureHMAC-SHA256 hex of the raw JSON body
X-Tulu-Switch-EventEvent name (e.g. deposit.success)
X-Tulu-Switch-TimestampISO 8601 timestamp of delivery

Signature Verification

Compute HMAC-SHA256(secret, rawBody) and compare it to the X-Tulu-Switch-Signature header using a timing-safe comparison. Always verify before processing an event.

Signature verification (TypeScript / Node.js)(TypeScript)
import crypto from 'crypto';

function verifyWebhook(rawBody: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Express example
app.post('/webhooks/tulu', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-tulu-switch-signature'] as string;
  if (!verifyWebhook(req.body.toString(), sig, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body.toString());
  // process event.event ...
  res.status(200).json({ received: true });
});
Parse the body as raw bytes before verification. Parsing to JSON first will alter whitespace and break the signature comparison.

Events Reference

deposit.success

A merchant deposit was confirmed by the provider.

deposit.failed

A merchant deposit failed or was rejected by the provider.

transfer.success

An outbound bank transfer (payout) was confirmed.

transfer.failed

An outbound bank transfer failed or was reversed.

bank_account.deposit.success

Funds arrived at a merchant or MFB virtual NUBAN account.

checkout.paid

A one-time virtual account (checkout) received payment.

customer.deposit.success

A customer wallet deposit was confirmed.

customer.deposit.failed

A customer wallet deposit failed.

subscription.charged

A recurring subscription cycle was charged successfully.

refund.completed

A refund was confirmed and credited by the provider.

Payload Structure

All webhook payloads share the same top-level envelope. The data object varies by event type.

deposit.success example(JSON)
{
  "event": "deposit.success",
  "data": {
    "id": "txn_xxx",
    "amount": 5000,
    "currency": "NGN",
    "status": "COMPLETED",
    "reference": "deposit_abc123",
    "customerId": "cus_yyy",
    "provider": "Paystack",
    "channel": "WALLET"
  },
  "timestamp": "2025-01-01T00:00:00.000Z"
}

event — the event name string

data — event-specific payload (transaction, customer, subscription, etc.)

timestamp — ISO 8601 datetime of when the event was fired

Retry Policy

If your endpoint does not return a 2xx status within 10 seconds, Tulu Switch will retry delivery:

AttemptDelay before retry
1st attemptImmediate
2nd attempt1 second
3rd attempt5 seconds
4th attempt (final)30 seconds

After 3 retries (4 total attempts), the delivery is marked as failed. Design your webhook handler to be idempotent — the same event may be delivered more than once.

Best Practices

  • Always verify the X-Tulu-Switch-Signature header before processing any event
  • Return a 200 response immediately, then process the event asynchronously
  • Make your handler idempotent — use the transaction or event ID to detect and skip duplicates
  • Keep your webhook secret in an environment variable; rotate it from the dashboard if it may have been exposed
  • Only use HTTPS endpoints — plain HTTP is rejected
  • Log all received events with the full payload for debugging and auditing