← The Playbook
Developer guide
A
Abby Sotomiwa
June 2026·8 min read

Webhooks vs polling for reward delivery: which to use and when

The architectural decision that determines whether your reward programme feels instant or feels broken. Here's when to use webhooks, when polling is acceptable, and the Africa-specific network considerations that change the calculus for developers building on unreliable infrastructure.

When a qualifying event fires in your system — a purchase is confirmed, a bet settles, a survey is submitted — your reward programme needs to know about it and act on it. How your system communicates that event to the reward infrastructure determines everything about the recipient experience: whether the reward arrives in 2 seconds or 2 hours, whether it arrives at all, and whether it arrives twice.

The two architectural patterns for this communication are webhooks and polling. Most developers are familiar with both in abstract. The considerations specific to African market infrastructure — network reliability, mobile money settlement patterns, payment gateway retry behaviour — add nuances that aren't in the generic documentation.

Polling: how it works and where it breaks

Polling is the simpler pattern to implement. Your system periodically queries the rewards API (or a status endpoint in your own system) to check whether qualifying events have occurred since the last check. Every N minutes or seconds, you ask: "has anything happened that should trigger a reward?" If yes, you issue the reward. If no, you wait for the next poll.

Polling pattern — simplified
// Every 60 seconds:
async function pollAndIssueRewards() {
  const events = await db.query(`
    SELECT * FROM qualifying_events
    WHERE processed = false
    AND created_at > NOW() - INTERVAL '2 minutes'
  `)

  for (const event of events) {
    await issueReward(event)
    await db.update('qualifying_events',
      { processed: true },
      { id: event.id }
    )
  }
}

setInterval(pollAndIssueRewards, 60_000)

Polling is easy to reason about and easy to debug. But it has two fundamental problems for reward delivery specifically:

First, latency. A 60-second poll cycle means the average reward delivery latency is 30 seconds — the average wait between event occurrence and next poll. For most use cases this is acceptable. For betting win bonuses, instant purchase rewards, and any mechanic where the emotional moment of the qualifying event matters, 30 seconds is too long. The bettor has already closed the app. The shopper has already left the checkout. The reward arrives into context-free void.

Second, resource waste. Polling queries your database and the rewards API constantly, whether or not anything has happened. At moderate volumes this is negligible. At high volumes — thousands of events per hour — the polling overhead becomes significant.

Webhooks: how they work and why they're better for rewards

Webhooks invert the polling pattern. Instead of your system asking "has anything happened?", external systems notify your endpoint the moment something happens. When a payment gateway confirms a purchase, it immediately fires an HTTP POST to your webhook endpoint. Your handler receives the event, processes it, and issues the reward — all within the same second as the payment confirmation.

Webhook handler — reward issuance
// Your webhook endpoint
app.post('/webhooks/payment-confirmed', async (req, res) => {
  // 1. Verify signature — always do this first
  const sig = req.headers['x-payment-signature']
  if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  // 2. Acknowledge immediately — before processing
  // Payment gateways retry if they don't get 200 within 5s
  res.status(200).json({ received: true })

  // 3. Process asynchronously
  const { payment_id, customer_phone, amount, currency } = req.body

  if (amount >= QUALIFYING_THRESHOLD && currency === 'NGN') {
    await issueReward({
      recipient: customer_phone,
      value: calculateRewardValue(amount),
      currency: 'NGN',
      category: 'grocery',
      channel: 'whatsapp',
      reference: payment_id,  // idempotency key
    })
  }
})

The critical detail in the code above: return HTTP 200 immediately, then process asynchronously. Payment gateways and event sources in African markets frequently have aggressive timeout settings — if your webhook endpoint takes more than 3–5 seconds to respond, they'll mark the delivery as failed and retry. If your reward issuance logic is synchronous in the webhook handler, a slow API response causes the gateway to retry, which causes duplicate event delivery, which causes duplicate reward issuance. This is the most common webhook implementation mistake in African integrations.

Africa-specific webhook considerations

Payment gateway retry behaviour in Nigerian and Kenyan markets

Nigerian payment gateways — Paystack, Flutterwave, Monnify — have documented retry policies for webhook delivery. Paystack retries failed webhook deliveries up to 15 times over 72 hours, with exponential backoff. Flutterwave retries with similar patterns. The implication: your webhook endpoint will receive the same event multiple times under normal failure conditions, not just abnormal ones.

M-Pesa's payment confirmation in Kenya has its own retry pattern — STK push confirmation events can fire more than once if the M-Pesa system doesn't receive your acknowledgement clearly. This is well-documented in Safaricom's developer documentation but frequently missed in implementations.

Every webhook handler processing reward triggers must be idempotent. The reward issuance API call must include an idempotency key derived from the original event ID — not a generated UUID created at handling time, which would be different on every retry.

01

Always use the source event ID as your idempotency key

The payment gateway event ID, the M-Pesa transaction reference, the bet settlement ID — whatever unique identifier the source system assigns to the event. This ID is stable across retries. A UUID generated at handling time is not.

02

Store processed event IDs in your database

Before calling the rewards API, check whether you've already processed this event ID. If yes, return 200 without issuing a reward. This gives you a local deduplication layer in addition to the API-level idempotency key.

03

Handle webhook signature verification for all sources

Every webhook source — payment gateways, your own event bus, the QIFTS platform — signs its webhook payloads. Verify the signature before processing. Unverified webhooks are a meaningful fraud surface: someone who can send a valid-looking payment confirmation to your webhook endpoint can trigger reward issuance without a real payment occurring.

Network unreliability and dead letter queues

Sub-Saharan African network infrastructure is more variable than European or North American infrastructure. Mobile network quality fluctuates. Server hosting in Lagos, Nairobi, and Accra (while improving rapidly) still sees higher packet loss rates than AWS us-east-1. Your webhook infrastructure needs to be designed for delivery failures, not just for the happy path.

The production-grade pattern for reward webhook handling in African markets:

Production webhook pattern with queue
// 1. Webhook endpoint — receive and enqueue only
app.post('/webhooks/payment', async (req, res) => {
  verifySignature(req) // throws if invalid
  await queue.push('reward_triggers', req.body) // fast
  res.status(200).json({ received: true })  // <200ms
})

// 2. Queue worker — process with retry logic
queue.process('reward_triggers', async (job) => {
  const { payment_id, phone, amount } = job.data

  // Idempotency check
  const exists = await db.exists('processed_events', payment_id)
  if (exists) return { skipped: true }

  // Issue reward
  const result = await qifts.issue({
    recipient: phone,
    value: rewardValue(amount),
    reference: payment_id,  // idempotency key
  })

  // Mark processed
  await db.insert('processed_events', {
    event_id: payment_id,
    reward_id: result.reward_id,
    processed_at: new Date(),
  })
})

// Worker has automatic retry with backoff on failure
// Dead letter queue captures events that fail after N retries

When polling is still appropriate

Polling isn't always wrong. There are specific reward programme contexts where it's the better or only option:

Batch reward programmes

Monthly loyalty recognition, end-of-quarter trade partner incentives, annual tenure milestones — programmes where the qualifying period is defined and batch processing is expected. A nightly job that identifies qualifying accounts and issues rewards is polling, and it's entirely appropriate here.

Legacy systems without webhook support

Some existing ERP, POS, or CRM systems in African markets don't support outbound webhook delivery. If your trigger source can't push events, you pull them. A polling job against the source system's database or API is the pragmatic solution.

Rate-limited event sources

Some event sources impose strict rate limits on outbound notifications. If you can only receive N notifications per minute, a controlled polling pattern may be more reliable than an uncontrolled webhook firehose.

The decision framework

Use polling when...
Use webhooks when...
Latency requirement
Minutes or hours is acceptable
Seconds matter — instant reward experience
Event source
No webhook support from source
Payment gateway, betting system, survey tool
Volume
Low — hundreds per day
High — thousands per hour
Programme type
Batch: monthly, quarterly rewards
Real-time: purchase triggers, win bonuses
Complexity tolerance
Simple to implement and debug
Requires idempotency, queue, signature verify

The practical recommendation

Build with webhooks from the start if your programme involves any real-time trigger events. The additional complexity of idempotency handling, signature verification, and queue-based processing is approximately one day of engineering work. The latency improvement — from minutes to seconds — changes the recipient experience fundamentally and cannot be retrofitted cheaply once a polling architecture is in production.

QIFTS API documentation

Webhook events, idempotency keys, and retry handling

Full documentation for QIFTS webhook events — all event types, payload schemas, signature verification, and retry behaviour.

Get started

Building the reward delivery integration?

The QIFTS API supports both webhook and polling patterns — with full idempotency, signature verification, and retry handling built in.