Skip to content

Integrate Wave

Wave's Checkout flow, the canonical webhook verification and the production checklist.

Wave Business offers a payment API called Checkout. This guide summarises how it works, then lists what you need before production. Provider sheet: Wave in the directory.

The Wave Checkout flow

Sources: Wave Checkout API and Wave Webhooks, checked on 1 October 2026.

  1. Your server creates a session: POST https://api.wave.com/v1/checkout/sessions with the Authorization: Bearer <Wave API key> header. Without it, Wave answers 401. Required fields: amount (string), currency (XOF), success_url and error_url (HTTPS). Useful field: client_reference, your order ID.

  2. Wave returns an id (prefix cos-) and a wave_launch_url. Redirect the customer to that address.

  3. The customer pays in Wave. The session has two statuses: checkout_status (open, complete, expired) and payment_status (processing, cancelled, succeeded).

  4. Wave sends a webhook: checkout.session.completed when the payment succeeds, checkout.session.payment_failed when it fails. Body: { "id": "EV_…", "type": "…", "data": { …session… } }.

  5. A session expires 30 minutes after creation. Refunds go through POST /v1/checkout/sessions/:id/refund (200 response, empty body).

The Checkout documentation we read describes no test environment. To test before production, ask Wave Business for access to its test environment.

Verify webhooks

This function is the canonical version, which the Orange Money and PayTech guides link to.

  • The header is called Wave-Signature and looks like t=…,v1=….
  • The signed message is the timestamp followed directly by the raw body, with no separator: HMAC-SHA256(<t><raw body>, secret), in hexadecimal.
  • During a key rotation the header may contain several v1 values. Accept the message if any of them matches.
  • Use the raw body, not re-serialised JSON: one extra space changes the signature.
  • Reject a message older than 5 minutes. Deduplicate on the event's id field.
  • A missing or malformed header is refused (400 response); it must not crash.
  • Another mode offered by Wave: a shared secret sent as Authorization: Bearer. Wave rates it as riskier.
JavaScript
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWaveSignature(header, rawBody, secret, toleranceS = 300) {
	if (typeof header !== 'string') return false;
	const pairs = header.split(',').map((p) => p.trim().split('='));
	const t = Number(pairs.find(([k]) => k === 't')?.[1]);
	const signatures = pairs.filter(([k, v]) => k === 'v1' && v).map(([, v]) => v);
	if (!Number.isFinite(t) || !signatures.length || Math.abs(Date.now() / 1000 - t) > toleranceS) return false;
	const expected = createHmac('sha256', secret).update(`${t}${rawBody}`).digest('hex');
	return signatures.some((s) => s.length === expected.length && timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

Keep a single function that translates Wave statuses into your own states.

Production checklist

  • Wave Business account opened. Only administrators see the Developers section of the Business Portal (directory sheet).
  • Registered company: RCCM and NINEA (procedure).
  • CDP declaration made and receipt received before processing personal data (procedure).
  • Your service's role regarding electronic money clarified (procedure).
  • Commercial terms and contract: not published in the documentation we read. Discuss them with Wave Business.
  • Real Wave base URL and key.
  • success_url, error_url and webhook URL over HTTPS.
  • Webhook secret stored outside the code, signature checked on every call.
  • Idempotent processing: the same event received twice delivers a single order.
  • Daily reconciliation, for example with the Balance & Reconciliation API.

What next?