Use Wavy on Your Website

This guide explains the end-to-end developer integration in detail: what to build, where to put each piece of logic, and how to make the flow reliable in production.

Guest-first checkout Flutterwave payment rail Webhook-driven settlement Idempotent updates

Prerequisites

  1. You have an integration public key created from the Wavy integration setup flow.
  2. You have a backend endpoint that can call Wavy APIs server-to-server.
  3. You have a webhook URL that is reachable publicly via HTTPS.
  4. Your order/subscription database has fields for status, payment_reference, payment_id, and last_event.
Plain words: before you start, make sure your server can talk to Wavy and your database can track one payment from start to finish.
Do not call create-payment from browser code using secrets. Trigger create-payment from your backend, then return checkout_url to the frontend.

Full Payment Sequence

1. Merchant Backend
POST /api/create-payment
2. Receive checkout_url
+ payment_id
3. Frontend Redirect
to hosted checkout
4. Customer Pays
via Flutterwave
5. Webhook + Callback
merchant updates records
Plain words: your system creates the payment, customer pays on Wavy, then Wavy tells your server the final result.
Source of truth for payment finality should be webhook events. Callback is primarily UX navigation.

Step 1: Create Payment (Backend)

Create a backend route like POST /checkout/start in your app. This route calls Wavy POST /api/create-payment.

Required Request Fields

  • amount: numeric value greater than 0
  • title: customer-visible payment title
  • description: payment purpose or invoice memo
  • callback_url: URL Wavy redirects customer to after checkout
  • metadata: structured context for your internal reconciliation
  • vendor_id: optional when payment should be linked to a specific vendor profile
Plain words: your server sends amount, title, and where to return the user after payment.
POST /api/create-payment Authorization: Bearer YOUR_INTEGRATION_PUBLIC_KEY Idempotency-Key: order-INV-1001 Content-Type: application/json { "amount": 5000, "title": "Pro Plan Renewal", "description": "Invoice INV-1001", "callback_url": "https://merchant.example.com/payments/callback", "metadata": { "order_id": "INV-1001", "customer_id": "cus_9292", "plan": "pro" }, "vendor_id": "optional" }

Expected Response Fields

  • payment_id
  • checkout_url
  • payment_link
{ "payment_id": "pay_2eb311", "checkout_url": "https://wavy.ng/pay/pay_2eb311", "payment_link": "https://wavy.ng/pay/pay_2eb311" }

Step 2: Redirect to Hosted Checkout (Frontend)

Your frontend should call your own backend start-payment route, receive checkout_url, then redirect.

async function startCheckout(orderId) { const res = await fetch('/checkout/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId }) }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Unable to initialize payment'); window.location.href = data.checkout_url; }
Plain words: user clicks pay, your frontend asks your backend for a payment URL, then sends user there.
Keep this step simple for guest checkout users. No account signup is required in customer flow.

Step 3: Handle Callback Return

After checkout, customer returns to your callback_url. Show a "processing" or "verifying" state first, then confirm status from your backend.

What To Do

  • Read reference/payment_id from callback params.
  • Call backend to fetch latest order/payment state.
  • Render success, failed, or pending state.

What To Avoid

  • Do not trust frontend query params as final proof.
  • Do not update order to paid from callback alone.
  • Do not skip webhook validation.
Plain words: callback page is for user feedback, not the final truth of payment status.

Step 4: Receive and Process Webhooks

Wavy sends webhook events to your backend. Handle each event type explicitly and log event + reference.

Events To Handle

  • payment.processing
  • payment.success
  • payment.failed
{ "event": "payment.success", "payment_id": "pay_2eb311", "amount": 5000, "reference": "WVY-INV-1001", "customer_email": "buyer@example.com", "metadata": { "order_id": "INV-1001", "plan": "pro" } }
Plain words: webhooks are machine-to-machine updates from Wavy to your server. Always verify signature before trusting them.
Verify webhook signature headers on your backend before processing the payload. Treat all unsigned payloads as untrusted.

Step 5: Idempotent Business State Updates

Use payment reference as your idempotency key when applying order/subscription updates.

Practical Rule Set

  1. If event is payment.processing and order is unpaid: mark order status "pending_payment".
  2. If event is payment.success and order not yet paid: mark paid, store paid_at, unlock value delivery.
  3. If event is payment.failed and order unpaid: mark failed and allow retry path.
  4. If event reference already processed: return 200 and do nothing else.
function processWebhook(event) { const key = event.reference; if (alreadyProcessed(key, event.event)) { return { ok: true, deduplicated: true }; } if (event.event === 'payment.success') { markInvoicePaid(event.metadata.order_id, key); } if (event.event === 'payment.failed') { markInvoiceFailed(event.metadata.order_id, key); } if (event.event === 'payment.processing') { markInvoicePending(event.metadata.order_id, key); } recordProcessedEvent(key, event.event); return { ok: true }; }
Plain words: if the same event comes twice, your system should safely ignore duplicates.

Split-Payment Behavior

Split payment is determined by link/vendor integration configuration. Frontend should communicate clearly when payment is routed via vendor split setup.

  • Guest links: simple payer flow, vendor split handled by backend/payment config.
  • Vendor dashboard links: split enabled based on vendor subaccount readiness.
  • Developer create-payment: optional vendor_id allows explicit vendor routing context.
Plain words: split behavior is configured in backend/integration settings, not by customer actions on checkout page.

Security Checklist

  • Never expose secret values in frontend bundles or HTML.
  • Use HTTPS callback and webhook endpoints only.
  • Validate and sanitize metadata before storing.
  • Implement request timeouts and retries for non-terminal errors.
  • Log payment_id, reference, event, and timestamp for audit traceability.
Plain words: keep secrets on server, use HTTPS everywhere, and always keep an audit log of payment events.

Error Handling and Retry Strategy

API Errors

  • 400: malformed request or validation failure
  • 401: invalid/expired token or key
  • 429: rate limit, retry with backoff
  • 503: temporary service issue, retry with backoff

Recommended Retry

  • Attempt 1: immediate
  • Attempt 2: +500ms
  • Attempt 3: +1000ms
  • Attempt 4: +2000ms then fail gracefully
Plain words: retry temporary failures, but do not keep retrying invalid payloads.
Do not retry permanently on 400-series validation errors. Fix payload and retry once corrected.

Testing and Go-Live Plan

  1. Create a low-value test payment and verify redirect works.
  2. Confirm callback page renders expected status.
  3. Verify webhook receives event and updates order idempotently.
  4. Replay same webhook payload and confirm no duplicate updates.
  5. Simulate failed and processing events for fallback UX.
  6. Run QA checklist before production rollout.
Plain words: test success, failure, and duplicate webhook scenarios before launch.