Developers
From zero to a verified webhook
Six steps, one API key and one signature check. Everything below runs against sandbox without a contract or a sales call.
Quick start
Six steps
Each one has a screen in the dashboard that walks you through it.
- 1
Create a project
A project gets a sandbox and a live environment. Credentials, keys and webhooks never cross between them.
- 2
Connect a provider
Paste the keys from your own Paystack, Flutterwave or Stripe account. Veyqo validates them against the environment before storing anything.
- 3
Create an API key
Scoped to one project and one environment. The secret is shown once and never again.
- 4
Initialize a payment
One POST. You get back a checkout URL to redirect the customer to.
- 5
Configure a webhook endpoint
Subscribe to the events you care about. Veyqo signs every delivery and retries with backoff.
- 6
Verify the signature
One HMAC check, identical for every provider. Then handle the event.
Create a payment
One request, one checkout URL
Amounts are always integers in the smallest currency unit — 50000 is ₦500.00. Send an Idempotency-Key and a retry can never double-charge.
curl https://api.veyqo.org/api/v1/payments \
-H "Authorization: Bearer $VEYQO_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "NGN",
"reference": "ORDER-1001",
"provider": "paystack",
"customer": { "email": "customer@example.com" },
"metadata": { "order_id": "1001" }
}'{
"success": true,
"data": {
"id": "pay_01KABCDXYZ",
"reference": "ORDER-1001",
"provider": "paystack",
"provider_reference": "q8df8fk2",
"status": "pending",
"amount": 50000,
"currency": "NGN",
"checkout": {
"url": "https://checkout.paystack.com/q8df8fk2"
},
"created_at": "2026-09-19T08:30:12.441Z"
}
}Hosted checkout
One script tag, every provider you connected
Customers pay by card, bank transfer or USSD on a Veyqo page, through the gateway you choose, and never get sent elsewhere. Create the session on your server so the amount cannot be changed, or start it in the browser with your publishable key.
<script src="https://veyqo.org/sdk/v1/checkout.js"></script>
<button id="pay">Pay ₦500.00</button>
<script>
document.getElementById('pay').onclick = async () => {
// Your server creates the session with its secret key.
const { id } = await fetch('/create-checkout', { method: 'POST' }).then((r) => r.json())
VeyqoCheckout.open({
session: id,
onSuccess: ({ reference }) => location.assign('/thanks?ref=' + reference),
onClose: () => console.log('Customer closed the checkout'),
})
}
</script>curl https://api.veyqo.org/api/v1/checkout/sessions \
-H "Authorization: Bearer $VEYQO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "NGN",
"reference": "ORDER-1001",
"provider": "flutterwave",
"customer": { "email": "customer@example.com" },
"success_url": "https://shop.example.com/thanks"
}'
# → { "data": { "id": "cs_…", "url": "https://…/pay/cs_…", "status": "open" } }- onSuccess fires after Veyqo verifies the payment with the provider.
- Fulfil on the payment.succeeded webhook, not the browser callback.
- Browser-started checkouts set their own amount: check it before you ship.
Webhooks
Verify Veyqo, not three providers
Every delivery is signed with HMAC-SHA256 over `{timestamp}.{raw body}`. Compare in constant time and reject anything older than five minutes.
{
"id": "evt_01KABCDXYZ",
"type": "payment.succeeded",
"created_at": "2026-09-19T08:31:04Z",
"environment": "live",
"data": {
"payment_id": "pay_01KABCDXYZ",
"reference": "ORDER-1001",
"amount": 50000,
"currency": "NGN",
"status": "succeeded",
"customer": { "email": "customer@example.com" },
"provider": { "name": "paystack", "reference": "q8df8fk2" },
"metadata": { "order_id": "1001" }
}
}const crypto = require('crypto');
function verify(rawBody, headers, secret) {
const signature = headers['veyqo-signature'];
const timestamp = headers['veyqo-timestamp'];
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? '');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
// Reject replays older than five minutes.
return Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
}- Use the raw request body — not a re-serialized object.
- Compare with a constant-time function, never ===.
- Respond 2xx quickly; do slow work after you acknowledge.
Reference
Every endpoint
Rendered from the same description the API console runs, so what you read here and what a request actually does cannot drift apart.
Errors
Stable codes, readable messages
Match on error.code — it never changes. Messages may be reworded for clarity.
| Code | HTTP | Meaning |
|---|---|---|
| INVALID_API_KEY | 401 | The key is missing, malformed, revoked or unknown. |
| INSUFFICIENT_PERMISSION | 403 | The key lacks the scope this endpoint requires. |
| ENVIRONMENT_MISMATCH | 403 | The resource belongs to a different environment. |
| PAYMENT_NOT_FOUND | 404 | No payment with that id or reference in this environment. |
| DUPLICATE_REFERENCE | 409 | That reference has already been used in this environment. |
| IDEMPOTENCY_KEY_REUSED | 409 | Same key, different request body. |
| VALIDATION_FAILED | 422 | See `error.details` for the offending fields. |
| PROVIDER_NOT_CONNECTED | 422 | That provider has no active connection here. |
| RATE_LIMIT_EXCEEDED | 429 | Back off and retry after `Retry-After` seconds. |
| PROVIDER_AUTHENTICATION_FAILED | 502 | The provider rejected your stored credentials. |
| PROVIDER_UNAVAILABLE | 503 | The provider timed out or returned a 5xx. |