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. 1

    Create a project

    A project gets a sandbox and a live environment. Credentials, keys and webhooks never cross between them.

  2. 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. 3

    Create an API key

    Scoped to one project and one environment. The secret is shown once and never again.

  4. 4

    Initialize a payment

    One POST. You get back a checkout URL to redirect the customer to.

  5. 5

    Configure a webhook endpoint

    Subscribe to the events you care about. Veyqo signs every delivery and retries with backoff.

  6. 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.

CodeHTTPMeaning
INVALID_API_KEY401The key is missing, malformed, revoked or unknown.
INSUFFICIENT_PERMISSION403The key lacks the scope this endpoint requires.
ENVIRONMENT_MISMATCH403The resource belongs to a different environment.
PAYMENT_NOT_FOUND404No payment with that id or reference in this environment.
DUPLICATE_REFERENCE409That reference has already been used in this environment.
IDEMPOTENCY_KEY_REUSED409Same key, different request body.
VALIDATION_FAILED422See `error.details` for the offending fields.
PROVIDER_NOT_CONNECTED422That provider has no active connection here.
RATE_LIMIT_EXCEEDED429Back off and retry after `Retry-After` seconds.
PROVIDER_AUTHENTICATION_FAILED502The provider rejected your stored credentials.
PROVIDER_UNAVAILABLE503The provider timed out or returned a 5xx.