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#
| Event | When it fires |
|---|---|
digipae.payment.completed | The customer paid. Close the sale and fulfil the order. |
digipae.payment.failed | A payment was attempted but failed. Release the cart. |
digipae.payment.expired | The 3-minute session TTL elapsed without payment. |
digipae.test_event | Synthetic event sent by the “Test webhook” button on the Integrations page. |
Payload#
{
"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#
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.
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");
});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
$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 eventRetries & reliability#
| Behavior | Value |
|---|---|
| Attempts | 3 per event (immediately, then after 2s and 5s) |
| Timeout | 10 seconds per attempt |
| Success | Any 2xx response from your endpoint |
| Manual replay | Replay 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.