Webhooks

When a payment session reaches a terminal state, Digipae POSTs a signed JSON event to the webhook URL you saved on the Integrations page. Webhooks are the source of truth for fulfilment — poll for UX, fulfil on the webhook.

Events#

EventWhen it fires
digipae.payment.completedThe customer paid. Close the sale and fulfil the order.
digipae.payment.failedA payment was attempted but failed. Release the cart.
digipae.payment.expiredThe 3-minute session TTL elapsed without payment.
digipae.test_eventSynthetic event sent by the “Test webhook” button on the Integrations page.

Payload#

Example · digipae.payment.completed
{
  "event": "digipae.payment.completed",
  "transaction_id": "tx_...",
  "order_ref": "ORDER-1042",
  "merchant_id": "DIG-7F82-Q4A1",
  "amount_cents": 2500,
  "currency": "USD",
  "fee_cents": 32,
  "net_cents": 2468,
  "status": "COMPLETED"
}

order_ref echoes the value you sent when creating the session — use it to find the matching order. net_cents is what settles to your balance after the flat fee.

Delivery headers#

Headers
X-Digipae-Signature: <hex>        // HMAC-SHA256 of the raw body
X-Digipae-Timestamp: <unix_ms>    // server time at delivery
X-Digipae-Merchant-Id: DIG-...    // your merchant display id
X-Digipae-Webhook-Source: merchant
X-Digipae-Replay: true            // present on manual replays only

Verifying signatures#

Compute HMAC-SHA256 over the raw request body using your webhook secret (shown when you save your webhook URL), then compare it to X-Digipae-Signaturewith a timing-safe comparison. Reject anything that doesn't match.

Node.js / Express
import crypto from "crypto";
import express from "express";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/digipae-webhook", (req, res) => {
  const signature = req.header("X-Digipae-Signature");
  const expected = crypto
    .createHmac("sha256", process.env.DIGIPAE_WEBHOOK_SECRET)
    .update(req.body) // raw bytes — do NOT JSON.parse first
    .digest("hex");

  const a = Buffer.from(signature ?? "", "utf8");
  const b = Buffer.from(expected, "utf8");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body.toString());
  switch (event.event) {
    case "digipae.payment.completed":
      // mark order paid
      break;
    case "digipae.payment.failed":
    case "digipae.payment.expired":
      // release the cart
      break;
  }
  res.status(200).send("ok");
});
Python / Flask
import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/digipae-webhook")
def digipae_webhook():
    secret = os.environ["DIGIPAE_WEBHOOK_SECRET"].encode()
    body = request.get_data()  # raw bytes
    expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Digipae-Signature", "")):
        abort(401)
    event = request.get_json()
    # ... handle event
    return "", 200
PHP
<?php
$secret = getenv("DIGIPAE_WEBHOOK_SECRET");
$body = file_get_contents("php://input"); // raw bytes
$expected = hash_hmac("sha256", $body, $secret);
$received = $_SERVER["HTTP_X_DIGIPAE_SIGNATURE"] ?? "";
if (!hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true);
// ... handle event
Verify over the raw bytes, before any JSON parsing. Re-stringifying a parsed body can change key order or whitespace and will break the signature.

Retries & reliability#

BehaviorValue
Attempts3 per event (immediately, then after 2s and 5s)
Timeout10 seconds per attempt
SuccessAny 2xx response from your endpoint
Manual replayReplay past events from the dashboard; replays carry X-Digipae-Replay: true

Respond 200 quickly and do heavy work async. Make your handler idempotent — treat transaction_id (or order_ref) as the dedupe key, since retries and replays can deliver the same event more than once.

Configuring your endpoint#

Set your HTTPS webhook URL under Dashboard → Integrations. Only https://URLs are accepted. Use the "Test webhook" button to send a digipae.test_event and confirm your signature verification end to end before going live.