Guides
Handle Webhooks
Receive, verify, and process OuiPay events safely.
Webhook receiving flow
sequenceDiagram
autonumber
participant OuiPay as OuiPay Event Engine
participant App as Merchant Server
participant Cache as Deduplication Store
participant Queue as Async Job Queue
OuiPay->>App: POST /webhooks/ouipay (X-OuiPay-Signature, X-OuiPay-Timestamp)
App->>App: 1. Verify HMAC SHA256 Signature
alt Invalid Signature or Timestamp > 5m
App-->>OuiPay: 401 Unauthorized
else Valid Request
App->>Cache: 2. Check event_key exists
alt Duplicate Event Key
Cache-->>App: Already processed
App-->>OuiPay: 200 OK (ignored duplicate)
else New Event Key
App->>Cache: Store event_key
App->>Queue: 3. Dispatch to worker queue
App-->>OuiPay: 200 OK (Acknowledged)
end
end1. Create a subscription
In the dashboard (or POST /v1/webhooks/subscriptions if your integration is
self-serve), register your endpoint and event types. Save the returned
signing secret: shown once.
2. Verify the signature
Reject anything that fails verification before any processing:
import crypto from "crypto";
function verify(
rawBody: string,
signature: string,
timestamp: string,
secret: string,
) {
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(`sha256=${expected}`),
Buffer.from(signature),
);
}Also reject X-OuiPay-Timestamp older than 5 minutes: replay protection.
3. Dedupe on event_key
Deliveries are at-least-once. Store processed event_keys and skip repeats
before any side-effect: a payment.completed replay must not fulfill twice.
4. Respond fast, process async
Return 200 quickly; do the work in a queue. A slow endpoint reads as a
failure and triggers retries.
app.post("/webhooks/ouipay", async (req, res) => {
if (
!verify(
req.rawBody,
req.headers["x-ouipay-signature"],
req.headers["x-ouipay-timestamp"],
SECRET,
)
) {
return res.status(401).end();
}
const { event_key, event_type, data } = req.body;
if (await alreadyProcessed(event_key)) return res.status(200).end();
await queue.push({ event_key, event_type, data });
res.status(200).end();
});5. Map events to your state
payment.completed→ mark the order paid / credit the servicetransfer.completed→ mark the payout done*.failed→ readfailure.code/failure.stage, surface the reason, release any reserved resourcewallet.funded→ credit the customer's balance in your system
Common pitfalls
- Verifying the parsed body: HMAC covers the raw bytes; verify before JSON parsing
- Trusting event order: use
occurred_atand transaction status, not arrival order - Blocking on slow work: ack fast, process async
- No dedupe: replays are normal;
event_keyis the guard