Skip to main content
Webhooks are the primary mechanism used by Rebell to notify merchant backends about final payment results and relevant state changes. While synchronous API responses (e.g., retailPay, createQrOrder, linkPayCreate) provide immediate feedback, webhooks are the source of truth for payment outcomes.
This guide covers how to implement webhook endpoints, verify their authenticity, handle retries, and integrate webhook events into your order management logic.

Why Webhooks Are Required

Rebell uses webhooks to:
  • Deliver final payment results (SUCCESS / FAIL)
  • Notify merchants about asynchronous status changes after PROCESSING
  • Provide a reliable, push-based channel for transaction updates
Webhooks are essential because of:
  • Network instability between merchant and Rebell
  • User actions happening after the initial API call
  • Risk checks that complete asynchronously
  • Multi-step payment approval flows

Webhook Endpoint Requirements

Your backend must expose an HTTPS endpoint capable of: Example endpoint URLs:
EnvironmentURL
Sandboxhttps://sandbox-merchant.com/rebell/webhook
Productionhttps://merchant.com/rebell/webhook

Webhook Payload Structure

A typical payment webhook payload:
{
  "eventType": "PAYMENT_RESULT",
  "paymentId": "2024032100123456",
  "paymentRequestId": "checkout-20240321-987",
  "paymentStatus": "SUCCESS",
  "paymentAmount": {
    "currency": "EUR",
    "value": 499
  },
  "paymentTime": "2024-03-21T10:15:33Z",
  "failureReason": null,
  "merchantData": {
    "externalStoreId": "STORE-77"
  }
}

Field Definitions

FieldDescription
eventTypeType of event (e.g., PAYMENT_RESULT)
paymentIdRebell payment identifier
paymentRequestIdMerchant’s original request/order ID
paymentStatusSUCCESS or FAIL
paymentAmountPayment amount and currency
paymentTimeTimestamp of finalization (ISO 8601)
failureReasonReason for failure (if any)
merchantDataOptional merchant-related fields (store, terminal, etc.)

Webhook Signature Verification

Rebell signs webhook POST requests using RSA, similar to how merchants sign API requests.

Request Headers

Webhook requests include:
  • Request-Time header
  • Signature header
  • JSON body

Signature Header Format

Signature: algorithm=SHA256withRSA, keyVersion=<n>, signature=<base64url>

Verification Steps

1

Extract Headers and Body

Extract from the incoming request:
  • Request-Time header
  • Signature header
  • Raw JSON body
2

Rebuild Signing String

Reconstruct the signing string (mirroring inbound API logic):
POST /<your_webhook_path>
<Rebell-Client-Id>.<Request-Time>.<BODY>
Where:
  • HTTP_PATH is your webhook path (e.g., /rebell/webhook)
  • Request-Time is the timestamp from the header
  • BODY is the raw JSON body
3

Verify Signature

Verify using Rebell’s public key:
  • Algorithm: SHA256withRSA
  • Use the correct Rebell public key for the keyVersion specified
4

Validate Timestamp

Reject if Request-Time is older than a defined window (e.g., 10 minutes) to prevent replay attacks
5

Process Webhook

Only after successful verification, process the webhook event
Important: If signature verification fails, do not process the event. Log it and return a non-2xx HTTP code.

Signature Verification Example

const crypto = require('crypto');

function verifyWebhookSignature(req, rebellPublicKey) {
  const requestTime = req.headers['request-time'];
  const signatureHeader = req.headers['signature'];
  const rawBody = req.rawBody; // Ensure you capture raw body

  // Parse signature header
  const signatureParts = {};
  signatureHeader.split(', ').forEach(part => {
    const [key, value] = part.split('=');
    signatureParts[key] = value;
  });

  const { algorithm, keyVersion, signature } = signatureParts;

  // Rebuild signing string
  const webhookPath = '/rebell/webhook';
  const signingString = `POST ${webhookPath}\n${requestTime}.${rawBody}`;

  // Verify signature
  const verifier = crypto.createVerify('SHA256');
  verifier.update(signingString);

  const signatureBuffer = Buffer.from(signature, 'base64url');
  const isValid = verifier.verify(rebellPublicKey, signatureBuffer);

  // Validate timestamp (10-minute window)
  const requestTimestamp = new Date(requestTime).getTime();
  const now = Date.now();
  const isTimestampValid = Math.abs(now - requestTimestamp) < 10 * 60 * 1000;

  return isValid && isTimestampValid;
}

Sequence Diagram

Idempotency & Repeated Deliveries

Webhooks are designed to be at-least-once delivery. This means the same event may be delivered multiple times.
Critical RequirementMerchant webhook handlers MUST be idempotent:
  • If paymentId already processed as SUCCESS → ignore repeated SUCCESS event
  • If paymentId processed as FAIL → ignore repeated FAIL event
  • Never process the same payment twice
  • Lock or transact order state changes atomically (db transaction / row-level locking)

Suggested Approach

1

Look Up Payment

Find the payment by paymentId or paymentRequestId
2

Check Status

If status is already FINAL (SUCCESS or FAIL), log and return HTTP 200
3

Update and Process

If status is not final, update it and proceed with business logic

HTTP Response Rules

ScenarioResponseRebell Behavior
Successful processingHTTP 200 OKNo retry
Temporary error (DB lock, transient)HTTP 5xxWill retry
Invalid signatureHTTP 400/401Will retry
Permanent errorHTTP 4xxMay retry
Performance RequirementEnsure your webhook endpoint responds quickly (under 2 seconds). Defer heavy processing to async workers if necessary to avoid timeouts.

Retry Policy

Rebell will retry webhook deliveries when:
  • HTTP status is not 2xx
  • Connection fails
  • TLS error occurs
Typical retry strategy:
  • Exponential backoff between retries
  • Limited number of retries (e.g., over 24 hours)
  • Retries stop once a 2xx response is received
Merchants should:

Complete Webhook Handler Example

const express = require('express');
const app = express();

// Capture raw body for signature verification
app.use('/webhooks/rebell', express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

app.post('/webhooks/rebell', async (req, res) => {
  // 1. Verify signature
  if (!verifyWebhookSignature(req, REBELL_PUBLIC_KEY)) {
    console.error('Invalid webhook signature');
    return res.status(401).send('Invalid signature');
  }

  // 2. Parse payload
  const {
    eventType,
    paymentId,
    paymentRequestId,
    paymentStatus,
    paymentAmount,
    paymentTime,
    failureReason
  } = req.body;

  // 3. Handle event type
  if (eventType === 'PAYMENT_RESULT') {
    try {
      // Find existing payment/order
      const order = await db.orders.findOne({
        where: { paymentId }
      });

      if (!order) {
        console.error(`Order not found for paymentId: ${paymentId}`);
        return res.status(200).send('OK'); // Don't retry for unknown orders
      }

      // Idempotency check - already processed?
      if (order.status === 'paid' || order.status === 'failed') {
        console.log(`Duplicate webhook for paymentId: ${paymentId}, ignoring`);
        return res.status(200).send('OK');
      }

      // Update order status
      if (paymentStatus === 'SUCCESS') {
        await db.orders.update(
          {
            status: 'paid',
            paymentTime,
            paidAmount: paymentAmount.value
          },
          { where: { paymentId } }
        );

        // Trigger fulfillment (async)
        await fulfillmentQueue.add({ orderId: order.id });

        console.log(`Payment SUCCESS for order: ${order.id}`);
      } else {
        await db.orders.update(
          {
            status: 'failed',
            failureReason
          },
          { where: { paymentId } }
        );

        console.log(`Payment FAILED for order: ${order.id}, reason: ${failureReason}`);
      }

      return res.status(200).send('OK');

    } catch (error) {
      console.error('Webhook processing error:', error);
      return res.status(500).send('Internal error'); // Trigger retry
    }
  }

  // Unknown event type - acknowledge anyway
  res.status(200).send('OK');
});

Security Best Practices

Security Requirements:
  • ✅ Expose webhook endpoint only via HTTPS
  • ✅ Verify signature on every webhook
  • ✅ Enforce IP allowlisting if supported/desired (optional)
  • ✅ Do not expose internal debugging info in HTTP responses
  • ✅ Log all failed verification attempts with high severity
  • ✅ Avoid logging sensitive data (payment details, tokens)
  • ✅ Use separate webhook secrets for sandbox vs production
  • ✅ Rotate Rebell public keys when notified

Handling Webhook Events

On SUCCESS

Payment completed successfullyActions to take:
  • Mark order as PAID in your system
  • Trigger order fulfillment
  • Issue tickets/receipts
  • Send confirmation notification to customer
  • Update frontend/POS if applicable

On FAIL

UX & Operational Considerations

Treat webhook as final authority for payment resultUse webhooks to trigger:
  • Order fulfillment
  • Ticket issuing
  • Receipt generation
  • Notifications to customer
Use Inquiry API only as a fallback when:
  • Webhook is delayed
  • Merchant needs active confirmation (e.g., POS timeout)

Testing Checklist

Test these scenarios in sandbox before going live:

Troubleshooting

Possible causes:
  • Webhook URL not configured in Rebell dashboard
  • Firewall blocking Rebell IPs
  • HTTPS certificate issues
  • Wrong environment (sandbox vs production)
Solutions:
  • Verify webhook URL in merchant dashboard
  • Check firewall rules and allowlist Rebell IPs
  • Ensure valid SSL certificate
  • Confirm environment settings match
Possible causes:
  • Using wrong Rebell public key
  • Key version mismatch
  • Body parsing modifying raw content
  • Incorrect signing string reconstruction
Solutions:
  • Verify you’re using the correct public key for the keyVersion
  • Ensure you capture the raw request body before JSON parsing
  • Double-check signing string format matches specification
This is expected behavior - webhooks are at-least-once delivery.Solutions:
  • Implement idempotent handling
  • Check payment status before processing
  • Use database transactions for state changes
Possible causes:
  • Heavy processing in webhook handler
  • Database connection issues
  • External service calls blocking response
Solutions:
  • Defer heavy processing to async workers/queues
  • Optimize database queries
  • Return 200 quickly, process asynchronously

Next Steps