Checkout API

Take Veriss payments on your website

Send customers from your site to a secure Veriss checkout page. They see who they're paying and how much, pay with Paystack (card, transfer, USSD and more), and come back to you with a signed result. Your server gets a signed webhook too.

Base URL

How it works

Every Veriss payment service can have an API payment link. Your website posts to it, and the customer finishes paying on Veriss. You never handle card details.

Your website

Create a checkout

POST the order (amount, reference, customer) to the API payment link.

Customer

Pays on Veriss

Sees your organization, the service and the amount, then pays with Paystack.

Veriss

Notifies you

Sends a signed payment.completed webhook to your server.

Customer

Returns to you

Back on your callback_url with a signed status. You confirm and deliver.

All amounts are whole numbers in kobo, the smallest unit of the naira: 500000 = ₦5,000. This is the same convention Paystack uses.

Quick start

  1. Sign in to your organization dashboard and open Developers.
  2. Create a secret key and store it on your server (e.g. VERISS_SECRET_KEY).
  3. Add your return domain (e.g. shop.example.com) and a webhook URL.
  4. Edit a service, turn on API checkout, and copy its API payment link.

Then create a checkout from your server and redirect the customer to checkout_url:

const crypto = require('crypto');
const SECRET = process.env.VERISS_SECRET_KEY; // vrs_sk_...

const serviceId = 'YOUR_SERVICE_ID';
const amount = 500000;          // kobo (₦5,000). Leave out for fixed-price services.
const reference = 'ORDER-1001'; // your order id: each one can be paid only once

const signature = crypto.createHmac('sha256', SECRET)
  .update([serviceId, amount, reference].join('|'))
  .digest('hex');

const res = await fetch(`{{BASE}}/${serviceId}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    amount, reference, signature,
    name: 'Ada Obi',
    email: 'ada@example.com',
    callback_url: 'https://shop.example.com/payment/return',
    metadata: { cartId: 'c_123' },
  }),
});
const { data } = await res.json();
// Send the customer to the hosted checkout page
response.redirect(303, data.checkout_url);

No server-side redirect? Use an HTML form

The API payment link also accepts a plain form post and redirects the customer straight to the checkout page. Your server still fills in the signature when it renders the form.

HTML
<form action="{{BASE}}/YOUR_SERVICE_ID" method="POST">
  <input type="hidden" name="amount" value="500000">
  <input type="hidden" name="reference" value="ORDER-1001">
  <input type="hidden" name="name" value="Ada Obi">
  <input type="hidden" name="email" value="ada@example.com">
  <input type="hidden" name="callback_url" value="https://shop.example.com/payment/return">
  <input type="hidden" name="signature" value="<?= $signature ?>">
  <button type="submit">Pay with Veriss</button>
</form>

Keys & settings

Everything is configured by the organization in Dashboard → Developers.

SettingWhat it does
Secret keyStarts with vrs_sk_. Signs your requests, and lets you check return URLs and webhooks and call the verify API. Shown once when created. Roll key replaces it, and the old key stops working at once.
Allowed return domainsHosts that callback_url may use (subdomains included), over https. http://localhost is allowed for testing.
Default webhook URLAn https endpoint on your server that receives payment.completed. Use Send test event to try it.
Webhook URL per serviceOptional, in the service's API checkout settings. That service's events go there instead of the default, so different systems can each get their own.
API checkout (per service)Gives the service an API payment link. Only accept signed requests is on by default.

Keep the secret key on your server. Never put it in browser JavaScript, mobile apps or a public repository.

Create a checkout

POST

Send application/json from your server to get JSON back, or post an HTML form (application/x-www-form-urlencoded) to be redirected (303) to the checkout page.

FieldDescription
amountDependsKobo, a whole number of at least 5000 (₦50). For fixed-price services leave it out or send exactly the price. For flexible services leave it out to let the customer enter the amount.
referenceRecommendedYour order id: letters, numbers and . _ : / -, up to 100 characters. A paid reference can't be paid again.
name, email, phone_numberOptionalPre-fill the customer's details on the checkout page.
callback_urlOptionalWhere to send the customer afterwards. Must be on an allowed domain over https.
metadataOptionalA JSON object (a JSON string in forms), up to 2,000 bytes. Returned by the verify API and in webhooks.
signatureIf requiredSee Signing requests.
Response · 201 Created
{
  "success": true,
  "data": {
    "session_id": "cs_9fJxQ2...",
    "checkout_url": "{{ORIGIN}}/checkout/cs_9fJxQ2...",
    "expires_at": "2026-10-12T10:52:01.000Z",
    "amount": 500000,
    "currency": "NGN",
    "merchant_reference": "ORDER-1001",
    "service": { "id": "YOUR_SERVICE_ID", "name": "Annual Dues" }
  }
}
  • A checkout link works for 1 hour.
  • Sending the same unpaid reference again returns the existing checkout (200), so a double click doesn't create two.
  • The service's access rules still apply to the customer's email, so services open to the public work best (Public only or Subscribers and the public).

Amounts

Service priceWhat to sendCustomer sees
FixedNothing, or exactly the priceThe fixed price
Flexible, amount from youamount (signed)Your amount, locked
Flexible, customer choosesNo amountAn amount field with the minimum shown
Closed fundraiserOptional amountUp to what's left to reach the target

The minimum is ₦50 (5000 kobo), the smallest charge Paystack accepts. Processing fees are added on top on the payment page. The organization receives the full amount.

Signing requests

A signature proves the request came from your server and that nobody changed the amount or reference in the browser.

signature = hex( HMAC-SHA256( secret_key, "<serviceId>|<amount>|<reference>" ) )

Use an empty string for anything you don't send. A fixed-price service with no amount signs "SERVICE_ID||ORDER-1001".

const signature = crypto.createHmac('sha256', process.env.VERISS_SECRET_KEY)
  .update([serviceId, amount ?? '', reference ?? ''].join('|'))
  .digest('hex');

If an organization turns off Only accept signed requests, a customer can edit the amount in your form. That's safe for fixed prices but not for flexible ones, so always check the amount with the verify API before you deliver.

Returning to your site

When the checkout ends, the customer is sent to your callback_url with these parameters added:

ParameterValue
statussuccess, cancelled or expired
session_idThe checkout session
referenceThe Veriss payment reference (VRS-…); empty if nothing was paid
merchant_referenceYour reference
amountKobo paid for the service (fees excluded)
signatureHMAC-SHA256 of session_id|status|reference|merchant_reference|amount
Node.js · Express
app.get('/payment/return', async (req, res) => {
  const { status, session_id, reference, merchant_reference, amount, signature } = req.query;
  const expected = crypto.createHmac('sha256', process.env.VERISS_SECRET_KEY)
    .update([session_id, status, reference, merchant_reference, amount].join('|'))
    .digest('hex');
  if (!signature || signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.status(400).send('Invalid signature');
  }
  if (status !== 'success') return res.redirect('/cart?payment=' + status);

  // Confirm with Veriss before delivering the order (see "Verify a payment")
  const order = await confirmAndFulfil(merchant_reference, reference);
  res.redirect('/orders/' + order.id);
});

The return is a convenience, not proof of payment: the customer controls the browser and might not come back. Rely on the webhook or the verify API.

Verify a payment

GET

Authenticate with Authorization: Bearer vrs_sk_…. Deliver the order only when status is success and merchant_reference and amount match it.

const r = await fetch(`{{BASE}}/transactions/${encodeURIComponent(reference)}`, {
  headers: { Authorization: `Bearer ${process.env.VERISS_SECRET_KEY}` },
});
const { data } = await r.json();
if (data.status === 'success' && data.merchant_reference === order.id && data.amount === order.amountKobo) {
  await markOrderPaid(order, data.reference);
}
  • status is pending, processing or success.
  • amount is what the organization receives. amount_charged is what the customer paid, including fees.

Find an order

GET

Returns the checkout for one of your orders: the paid one if there is one, otherwise the latest. Use it when a customer never came back and no webhook arrived.

Response
{
  "success": true,
  "data": {
    "session_id": "cs_75eMdP2o9QwhJwoiQwsIG4u3",
    "status": "paid",            // open | processing | paid | cancelled | expired
    "merchant_reference": "ORDER-1001",
    "amount": 500000,
    "created_at": "2026-10-12T08:51:40.112Z",
    "transaction": { "reference": "VRS-...", "status": "success", ... }
  }
}

Webhooks

As soon as a payment succeeds, Veriss sends POST to your webhook URL. This happens even if the customer closes their browser.

HeaderValue
X-Veriss-Eventpayment.completed (or ping for test events)
X-Veriss-SignatureHex HMAC-SHA256 of the raw request body with your secret key
app.post('/webhooks/veriss', express.raw({ type: 'application/json' }), async (req, res) => {
  const expected = crypto.createHmac('sha256', process.env.VERISS_SECRET_KEY)
    .update(req.body) // raw bytes, before JSON.parse
    .digest('hex');
  const received = String(req.get('X-Veriss-Signature') || '');
  if (received.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
    return res.sendStatus(401);
  }
  const { event, data } = JSON.parse(req.body);
  if (event === 'payment.completed') {
    await markOrderPaid(data.merchant_reference, data); // idempotent: it can arrive more than once
  }
  res.sendStatus(200);
});
  • Reply with any 2xx within 10 seconds. Redirects aren't followed.
  • Failed deliveries are retried after 1, 5 and 30 minutes.
  • duplicate: true means the customer paid a checkout that was already paid (e.g. in two tabs). Consider refunding it.

Errors

Errors are JSON: { "success": false, "code": "…", "message": "…" }. A rejected HTML form post shows a Veriss error page with the same message instead.

CodeHTTPMeaning
signature_required401The service only accepts signed requests
invalid_signature401The signature doesn't match the amount, reference and key
invalid_amount400Not whole kobo, below ₦50, or above a fundraiser's remaining target
amount_mismatch400Amount sent for a fixed-price service doesn't equal the price
callback_url_not_allowed400The return domain isn't on your allowed list
invalid_callback_url400Not a full https URL
invalid_reference, invalid_email, invalid_phone, invalid_metadata400Field format
already_paid409This reference was already paid; data.reference is the payment
service_inactive409The service is suspended, ended or not started yet
api_checkout_disabled403API checkout is off for this service
api_not_configured403The organization hasn't created a secret key
service_not_found404Unknown service id
unauthorized401Missing or wrong secret key (verify and lookup)
—429Too many requests; wait for Retry-After seconds

Go-live checklist

  • The secret key is in a server environment variable, not in code or the browser.
  • Requests are signed, and Only accept signed requests is on for the service.
  • Amounts are sent in kobo (₦5,000 = 500000).
  • Every order has a unique reference.
  • Your return page checks the signature, then confirms with the verify API.
  • Your webhook endpoint checks X-Veriss-Signature and handles repeats safely. Send test event succeeds.
  • You compare amount and merchant_reference with your order before delivering.
  • You've made one real payment end to end and checked it in Developers → Recent API checkouts.

Security notes

  • Signing needs a server. A purely static website can't keep a secret key. Use a small serverless function to sign, or accept unsigned requests and always verify the amount.
  • Roll your key right away if you think it has leaked. The old key stops working immediately, so update your servers at the same time.
  • Double payments are possible. A customer who pays the same checkout twice (two tabs) is charged twice. Both payments are reported, the second with duplicate: true.
  • The webhook can be late or arrive twice. If it hasn't arrived, use lookup or verify, which are always up to date.

FAQ

Do customers need a Veriss account?

No. They enter their name and email on the checkout page and get a receipt.

Who pays the fees?

The customer. Processing and platform fees are added on top of the amount, so the organization receives the full amount.

Can I test without real money?

Use a low-priced test service and a small amount (₦50 is the minimum), or a separate test environment run with Paystack test keys.

What does the customer see?

Your organization's name (with its verified check mark, if verified), the service, the amount, your order reference and a Cancel and return link back to your site.

Ready to integrate?

Sign in to create your secret key and turn on API checkout for a service.

Get your keys