Safe to fulfil
checkout.session.completed, terminal success, matching amount, matching currency, and matching invoice reference.
Merchant API · Public contract 2026-06
Guides, examples, and API reference for creating hosted checkout sessions, redirecting payers, and confirming final payment state.
https://api.staging.velorapay.comExplore by product area
Use the Merchant Dashboard to create the integration, API key, and webhook.
Prepare invoice dataRecord service-code line items against your invoice reference.
Create a hosted checkout sessionUse POST /api/v1/sessions to issue a hosted payment link.
VeloraPay renders the enabled methods and collects the payment.
Confirm paymentUse signed webhooks first, with session lookup as a backend fallback.
Release notes and versioningTrack additive changes, breaking-change policy, and migration notes.
Integration setup
Create the integration, test API key, and webhook receiver in the VeloraPay Merchant Dashboard. Your backend then uses the generated API key for the payment APIs below.
Complete setup in the dashboard, then store generated secrets on your server.
Merchant Dashboard
Select integration
Name the app, ERP, website, or back-office system.
Merchant Dashboard
Create API key
Copy the one-time key into your backend secret manager.
Merchant Dashboard
Add webhook
Register the HTTPS receiver and store the signing secret.
Use the Merchant Dashboard to create the container for the app or system that will connect to VeloraPay.
API keys are scoped to one integration. Copy the one-time key into your backend secret manager and use it only from your server.
Register your HTTPS receiver in the Merchant Dashboard. Copy the one-time signing secret into your webhook verifier before accepting payment events.
After dashboard setup, call the payment APIs below with
Authorization: Bearer ak_test_example in staging. Do not
expose merchant API keys in browser or mobile clients.
Build your first payment
This is the standard hosted-checkout path: create the session from your backend, send the payer to VeloraPay, then confirm the final state before fulfilment.
Make authenticated calls from your backend. Use webhooks as the primary confirmation signal.
POST /api/v1/invoice-lines
Record invoice lines
Attach service-code line items to your invoice reference.
POST /api/v1/sessions
Create checkout
Return a hosted checkout URL for the payer.
checkout_url
Send the payer to hosted checkout
VeloraPay collects the selected payment details, including hosted card entry.
VeloraPay checkout
VeloraPay collects payment
Hosted checkout handles payer prompts, card entry, transfer details, and slips.
checkout.session.completed
Receive webhook
Use the signed event as the primary fulfilment signal.
GET /api/v1/sessions/{session_id}
Fallback lookup
Use server-side session lookup if the payer returns before the webhook arrives.
amount_minor
Verify final state
Match amount, currency, invoice reference, and terminal status.
Use the same invoice_ref when you create the checkout
session. Invoice lines help the payer recognize the bill and help your
finance team settle collections by service code.
curl https://api.staging.velorapay.com/api/v1/invoice-lines \
-H "Authorization: Bearer ak_test_example" \
-H "Idempotency-Key: invoice-EPA-2026-001" \
-H "Content-Type: application/json" \
-d '{
"invoice_ref": "EPA-2026-001",
"currency": "GHS",
"lines": [
{ "code": "EPA-PERMIT", "name": "Permit fee", "amount_minor": 300000 },
{ "code": "EPA-LEVY", "name": "Environmental levy", "amount_minor": 7038 }
]
}'
{
"object": "invoice.line_items",
"invoice_ref": "EPA-2026-001",
"ingested": 2,
"billed_minor": 307038,
"errors": []
}
The response includes checkout_url. Redirect the payer
there, or send the link through your own payment notification flow.
const response = await fetch("https://api.staging.velorapay.com/api/v1/sessions", {
method: "POST",
headers: {
"Authorization": "Bearer ak_test_example",
"Idempotency-Key": "checkout-EPA-2026-001",
"Content-Type": "application/json"
},
body: JSON.stringify({
invoice_ref: "EPA-2026-001",
amount_minor: 307038,
currency: "GHS",
description: "EPA permit payment",
customer: { name: "Kwame Asante", email: "kwame@example.com" },
callback_url: "https://epa.gov.gh/payments/return"
})
});
const session = await response.json();
return session.checkout_url;
import requests
response = requests.post(
"https://api.staging.velorapay.com/api/v1/sessions",
headers={
"Authorization": "Bearer ak_test_example",
"Idempotency-Key": "checkout-EPA-2026-001",
},
json={
"invoice_ref": "EPA-2026-001",
"amount_minor": 307038,
"currency": "GHS",
"description": "EPA permit payment",
"customer": {"name": "Kwame Asante", "email": "kwame@example.com"},
"callback_url": "https://epa.gov.gh/payments/return",
},
timeout=15,
)
response.raise_for_status()
checkout_url = response.json()["checkout_url"]
curl https://api.staging.velorapay.com/api/v1/sessions \
-H "Authorization: Bearer ak_test_example" \
-H "Idempotency-Key: checkout-EPA-2026-001" \
-H "Content-Type: application/json" \
-d '{
"invoice_ref": "EPA-2026-001",
"amount_minor": 307038,
"currency": "GHS",
"description": "EPA permit payment",
"customer": { "name": "Kwame Asante", "email": "kwame@example.com" },
"callback_url": "https://epa.gov.gh/payments/return"
}'
{
"id": "cs_example",
"status": "open",
"amount_minor": 307038,
"currency": "GHS",
"invoice_ref": "EPA-2026-001",
"description": "EPA permit payment",
"checkout_url": "https://velorapay-checkout-staging-fxzcszzkha-uc.a.run.app/pay/cs_example",
"expires_at": "2026-06-14T12:30:00Z"
}
Set callback_url to your return route and store the
VeloraPay session id, invoice_ref, and your order id
before redirecting the payer. After checkout completes, VeloraPay
can return the payer to EPA's callback URL. EPA must still confirm
final state by webhook or server-side session lookup before marking
the order paid.
After you redirect the payer to checkout_url,
VeloraPay-hosted checkout handles payment-method selection, fee
display, Mobile Money prompts, card entry, cash slip creation, and
payment collection. Your backend waits for a webhook or session
lookup; it does not start payment-method calls.
Signed webhooks are the primary fulfilment signal. If EPA needs a
fallback lookup after the payer returns, call the merchant session
endpoint with the integration API key. This path does not require an
attempt_reference.
curl https://api.staging.velorapay.com/api/v1/sessions/cs_example \
-H "Authorization: Bearer ak_test_example"
{
"id": "cs_example",
"status": "pending",
"amount_minor": 307038,
"currency": "GHS",
"invoice_ref": "EPA-2026-001",
"description": "EPA permit payment",
"checkout_url": "https://velorapay-checkout-staging-fxzcszzkha-uc.a.run.app/pay/cs_example",
"expires_at": "2026-06-14T12:30:00Z"
}
A return to your site is not proof of payment. Fulfil only after the signed webhook or final checkout status confirms the payment amount, currency, invoice reference, and terminal state.
checkout.session.completed, terminal success, matching amount, matching currency, and matching invoice reference.
Pending, failed, expired, amount mismatch, currency mismatch, duplicate event with changed body, or invalid signature.
Authentication and retries
Merchant API keys identify your account and must stay on your server or in a secret manager. VeloraPay checkout pages use public session endpoints and never need your key.
Use separate staging and go-live credentials. Rotate a key if it is ever exposed in app code, logs, screenshots, or support tickets.
Send a stable idempotency key on checkout and invoice-line writes. If a network call fails, retry with the same key and same body.
Invoice model
VeloraPay links each checkout to your invoice_ref. Revenue
lines name the service codes you collect for; invoice lines attach amounts
to those codes for one bill.
Sync the service codes your organisation collects against.
Record the exact amount due for each code on one invoice.
Create the payer page and lock the amount to collect.
Payment flow
Mobile Money, bank transfer, and cash confirmation can be asynchronous. Your system should treat the checkout as pending until VeloraPay returns a terminal status through a signed webhook or server-side session lookup.
The payer is on VeloraPay-hosted checkout and can choose an enabled payment method.
A payer action, provider update, or teller review is still in progress.
Payment is final for fulfilment after amount, currency, and invoice reference match.
Do not release value. Ask the payer to create a new payment path if needed.
Payment methods
For standard merchant integrations, your backend does not quote,
start, or poll payment-method calls. Redirect the payer to
checkout_url; VeloraPay checkout shows enabled methods,
computes fees, collects payer input, and drives the payment.
Hosted checkout collects the payer's network and number, then asks them to approve or verify the payment.
Hosted checkout endpointsHosted checkout returns account details for this invoice and confirms when the transfer is matched.
Read finality rulesHosted checkout creates or reuses one active deposit slip. A teller review can make the checkout pending before final approval.
Cash deposit guideCard entry belongs on VeloraPay-hosted checkout. Do not collect card numbers or security codes in your merchant flow.
Go-live requirementComplete through hosted checkout. The payer enters network and phone details there, then approves or verifies the payment before VeloraPay sends a terminal update.
Hosted checkout shows the account instructions and waits for matched status or a terminal webhook before fulfilment.
Hosted checkout creates one active slip for an open checkout. Teller review can keep the checkout pending before the final completed event.
Redirect to hosted checkout. Do not collect card numbers, security codes, or cardholder authentication data in your merchant application.
Cash deposits
Retrying a cash deposit slip request returns the active slip with
replayed: true. If a teller is already reviewing the deposit,
ask the payer to wait instead of issuing another slip.
{
"slip_token": "SLIP-ABC123DEF456",
"reference": "s-cash-example",
"amount_minor": 307038,
"currency": "GHS",
"invoice_ref": "EPA-2026-001",
"customer_name": "Kwame Asante",
"status": "active",
"expires_at": "2026-06-15T12:00:00Z",
"replayed": true
}
Webhooks
Register the HTTPS receiver in the Merchant Dashboard and store the signing secret in your backend secret manager. Verify every delivery before you update an order, release a service, or mark an invoice as paid.
Webhook URL: https://merchant.example/webhooks/velorapay
Events: checkout.session.completed, checkout.session.failed, checkout.session.expired
Secret storage: backend secret manager
Create, rotate, or disable webhook endpoints from the Merchant Dashboard.
Copy the signing secret into your secret manager before accepting payment events.
Fulfil only after a valid terminal event matches the invoice reference, amount, and currency.
Each delivery includes X-VeloraPay-Event-Id,
X-VeloraPay-Event-Type, and
X-VeloraPay-Signature.
Expect checkout.session.completed,
checkout.session.failed, and
checkout.session.expired.
Confirm the session status, amount_minor,
currency, and invoice_ref before releasing
goods or services.
X-VeloraPay-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>
v1 = HMAC_SHA256(signing_secret, "<t>.<raw_request_body>")
Use the raw request body exactly as received. Do not verify a parsed and
re-stringified JSON object.
{
"id": "evt_example",
"type": "checkout.session.completed",
"data": {
"session": {
"id": "cs_example",
"status": "success",
"amount_minor": 307038,
"currency": "GHS",
"invoice_ref": "EPA-2026-001",
"metadata": {}
}
}
}
1. Read the raw request body.
2. Parse t and v1 from X-VeloraPay-Signature.
3. Reject timestamps more than 5 minutes from your server clock.
4. Recompute HMAC_SHA256(signing_secret, "<t>.<raw_request_body>").
5. Compare signatures with a constant-time comparison.
6. Store X-VeloraPay-Event-Id and return 200 for already-processed events.
7. Treat delivery as at-least-once; your handler must be idempotent.
8. For checkout.session.completed, confirm amount_minor, currency,
invoice_ref, and terminal status before fulfilment.
import crypto from "node:crypto";
function verifyVeloraPayWebhook({ rawBody, signature, signingSecret }) {
const parts = Object.fromEntries(signature.split(",").map((part) => part.split("=")));
const timestamp = Number(parts.t);
const received = parts.v1 || "";
const ageSeconds = Math.abs(Date.now() / 1000 - timestamp);
if (!timestamp || ageSeconds > 300) throw new Error("stale webhook");
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
if (
received.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))
) {
throw new Error("invalid webhook signature");
}
}
export async function handleVeloraPayWebhook(request, response) {
const rawBody = await getRawBody(request);
verifyVeloraPayWebhook({
rawBody,
signature: request.headers["x-velorapay-signature"],
signingSecret: process.env.VELORAPAY_WEBHOOK_SECRET
});
const eventId = request.headers["x-velorapay-event-id"];
if (await alreadyProcessed(eventId)) return response.status(200).end();
const event = JSON.parse(rawBody);
if (event.type === "checkout.session.completed") {
await fulfilIfAmountCurrencyAndInvoiceMatch(event.data.session);
}
await markProcessed(eventId);
response.status(200).end();
}
import hmac
import json
import time
from hashlib import sha256
def verify_velorapay_webhook(raw_body: bytes, signature: str, signing_secret: str) -> None:
parts = dict(item.split("=", 1) for item in signature.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > 300:
raise ValueError("stale webhook")
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(signing_secret.encode(), signed_payload, sha256).hexdigest()
if not hmac.compare_digest(parts.get("v1", ""), expected):
raise ValueError("invalid webhook signature")
def handle_velorapay_webhook(request):
raw_body = request.body
verify_velorapay_webhook(
raw_body,
request.headers["X-VeloraPay-Signature"],
VELORAPAY_WEBHOOK_SECRET,
)
event_id = request.headers["X-VeloraPay-Event-Id"]
if already_processed(event_id):
return "", 200
event = json.loads(raw_body)
if event["type"] == "checkout.session.completed":
fulfil_if_amount_currency_and_invoice_match(event["data"]["session"])
mark_processed(event_id)
return "", 200
Staging testing
Use staging API keys, synthetic invoices, test payer details, and signed webhook samples. Staging supports test integrations and test API keys only. Keep screenshots and support tickets free of real customer or payment data.
Use a unique invoice reference and service-code lines for each test.
Exercise success, pending, failure, retry, and expiry paths through hosted checkout.
Confirm dedupe by sending the same event id more than once.
Match checkout, invoice, webhook, and settlement evidence by invoice_ref.
Staging workspace
Keep test data synthetic and repeatable. The staging collection includes the same invoice reference, idempotency keys, payer fixture, return route, and webhook receiver shape used throughout this guide.
ak_test_example
EPA-2026-001
GHS
300 seconds
https://api.staging.velorapay.com
Create invoice lines, create checkout, send the payer to hosted checkout, receive checkout.session.completed, and fulfil only after the amount, currency, and invoice reference match.
Use hosted checkout for Mobile Money or bank transfer, keep the order pending while finalized is false, and show the payer a recoverable status.
Confirm failed or expired sessions never release value and can be restarted with a new payment path when the merchant policy allows it.
Send the same event id twice. The second delivery must return 200 without fulfiling the invoice again.
Change one byte in the raw body or timestamp. Your handler must reject the delivery before touching order state.
Retry session and invoice-line writes with the same Idempotency-Key and body. Reusing the key with a different body must fail.
Versioning and changelog
The public API is documented as an explicit versioned contract. New optional response fields can be added without notice, but required request fields, event names, webhook signature rules, and error-code meanings need a migration note before they change.
Checkout sessions, invoice lines, hosted payment methods, cash deposits, webhooks, errors.
Clearer setup, redirect, webhook, fallback lookup, and staging-test guidance.
Ship an upgrade path before changing required fields or finality semantics.
Errors
Error responses use the same envelope across the merchant API. Use the code to decide whether to retry, refresh the checkout, or ask an operator to correct the request.
retry_after_seconds before retrying.
The full error-code enum is published in the OpenAPI component
MerchantErrorCode.
Finality and settlement
A successful checkout means the payment has reached VeloraPay's final collection state for fulfilment. Payout, settlement, and merchant reconciliation records can arrive later and should be used for finance operations, not for first fulfilment.
Requires terminal success plus matching amount, currency, and invoice reference.
Confirm the supported process with VeloraPay before promising a reversal or refund flow.
Use invoice_ref, checkout id, event id, and payment rail to match finance records.
Developer assets
The guide explains the payment journey. The reference and machine-readable assets carry exact endpoint fields, response shapes, and contract enums.
Endpoint fields, examples, status codes, and schemas.
OpenAPI Download JSON schemaUse for client generation and contract review.
Markdown Merchant guide markdownPlain-text guide for code review and server-side setup.
LLMs llms.txtCompact contract map for tooling.
Collection Staging request collectionImportable examples for the core payment path.
Server examples Node, Python, and curlSDK-style snippets for backend-only checkout creation.
Go-live checklist
Before go-live, prove the full checkout path, signed webhooks, cash review, reconciliation evidence, key storage, and support workflow with test data.
Reference
Use the reference when you are mapping request fields, handling response states, or wiring generated clients.