DevelopersAnyBanQ Pay
AnyBanQ Pay developer guide
Verify a customer, collect from their Sri Lankan bank account, take a remittance from abroad and pay many accounts at once. One library on your server, one tag in your page, and signed webhooks back to you.
Read this first
What is live today
Not everything on this page moves real money yet. Build against all of it; plan a launch only around the rows marked live.
Identity verification
LiveHosted sessions run the workflow you published. Which checks reach a real provider depends on the checks in it.
Collection from a local bank account
LiveOne rail: HNB JustPay. The customer links an HNB account on the hosted pay page and approves the debit there.
Card collection
Not liveNo card acquirer is connected. A customer cannot pay a bill, a link or a remittance order with a real card.
Remittance
Test modeOrders are collected on a test card page and no money moves. Each one is marked sandbox: true.
Payouts to bank accounts
Not liveA remittance payout and every row of a payout batch are recorded as DRY_RUN. Nothing is sent to a bank.
Remittance and bulk payouts are also switched on per organisation. Until they are on for yours, their endpoints answer 403. See which services each area needs.
How it fits
A key on your server, a token in the page, a webhook back
- 1
Your server creates the thing
A verification session, a payment link, a remittance order or a payout batch, using your API key. The key never leaves your server.
- 2
Your server hands the page a token
A session token or a checkout token. It names one verification or one order, shows only what the customer needs to see, and expires.
- 3
The page opens our hosted flow
anybanq.js opens it in a frame served from our origin. Identity documents, bank details and card numbers are entered there, not on your page.
- 4
We tell your server what happened
A webhook, signed with your webhook secret. Treat a browser callback as a prompt to refresh; treat the webhook, or a read with your key, as the fact.
Before you start
A key, your domains, the library
1. Get an API key
Sign in to the console and open API keys. Create a key and copy it then: it is shown once and we keep only a hash. A key that starts divp_test_ works while your account is still being reviewed, and what it makes is a test record. A key that starts divp_live_ needs an approved account.
Send it as a bearer token on every request to https://verifia.anybanq.lk/v1. You can narrow a key to scopes when you create it; the table below lists the scope each area needs. A scoped key reaches only what its scopes cover; an unscoped key can do everything a key is allowed to. No key can manage keys, webhooks, your team or your settings.
curl https://verifia.anybanq.lk/v1/remittance/orders \
-H "Authorization: Bearer $ANYBANQ_API_KEY"2. List the domains that may embed us
The verification flow and the remittance checkout open inside a frame on your page. We refuse to be framed by a site you have not named, so add your site first: in the console open Workflows, open a workflow, and fill in the allowed domains under Share & Embed. Enter bare hosts such as app.example.lk, or *.example.lk. Only https pages are allowed. The list is one per organisation and covers both flows.
The pay button and payment links need no entry: they send the customer to our pay page in a tab of its own, which is never framed.
3. Download the server library
The library is a file you download and keep in your project. It is not on npm and there is nothing to install. The files are in Downloads, just below.
4. Add the browser tag
<script src="https://verifia.anybanq.lk/sdk/v1/anybanq.js"></script>Downloads
One file for Node, a collection for everything else
On a Node server, take the library: one JavaScript file, Node 18 or newer, no dependencies and no install step. On any other language, take the API collection and call the endpoints directly; every call on this page is also shown as raw HTTP.
- Download anybanq-pay.mjs
Server library, ES modules
anybanq-pay.mjs
For a project that uses import. One file, no dependencies.
- Download anybanq-pay.cjs
Server library, CommonJS
anybanq-pay.cjs
The same library for a project that uses require.
- Download anybanq-pay.d.ts
TypeScript types
anybanq-pay.d.ts
Optional. Save it beside the library as anybanq-pay.d.mts (with the .mjs file) or anybanq-pay.d.cts (with the .cjs file): TypeScript looks for a declaration named after the module.
- Download anybanq-pay.openapi.json
API collection, OpenAPI 3.1
anybanq-pay.openapi.json
For any other language. Only the endpoints on this page and the webhook payloads, with an example for every request and response.
- Download anybanq-pay.postman_collection.json
Postman collection
anybanq-pay.postman_collection.json
The same requests, with variables for the host and your key, and signed example webhooks you can send to your own endpoint.
curl -O https://verifia.anybanq.lk/sdk/v1/server/anybanq-pay.mjs
curl -O https://verifia.anybanq.lk/sdk/v1/server/anybanq-pay.cjs
curl -O https://verifia.anybanq.lk/sdk/v1/server/anybanq-pay.d.ts
curl -O https://verifia.anybanq.lk/sdk/v1/server/anybanq-pay.openapi.json
curl -O https://verifia.anybanq.lk/sdk/v1/server/anybanq-pay.postman_collection.jsonUse it
Put the file in your project and import it by its path. The first lines of each file carry its version and build date.
import { AnybanqPay } from "./anybanq-pay.mjs";
const pay = new AnybanqPay({ apiKey: process.env.ANYBANQ_API_KEY });const { AnybanqPay } = require("./anybanq-pay.cjs");
const pay = new AnybanqPay({ apiKey: process.env.ANYBANQ_API_KEY });The samples below are written for the ES module file. With CommonJS, replace each import { … } from "./anybanq-pay.mjs" with const { … } = require("./anybanq-pay.cjs"); everything else is the same.
To update, download the file again and replace your copy. Check the version in its header against the one you have.
1 · Identity
Verify a customer
Build and publish a workflow in the console: it decides which checks run and in what order. Then start one session per customer. The session's token is what the browser gets.
const session = await pay.verifications.createSession(WORKFLOW_ID, {
subjectRef: "customer-42", // your own reference, up to 120 characters
ttlHours: 2, // default 24
});
// Give the page the token and nothing else.
res.json({ token: session.token });
// No embed? Send the customer to session.hostedUrl instead.curl -X POST https://verifia.anybanq.lk/v1/workflows/$WORKFLOW_ID/sessions \
-H "Authorization: Bearer $ANYBANQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "subjectRef": "customer-42", "ttlHours": 2 }'
# 201
# { "id": "…", "token": "vs_…", "status": "PENDING", "subjectRef": "customer-42",
# "expiresAt": "…", "hostedUrl": "https://verifia.anybanq.lk/s/vs_…", "embedUrl": "…" }<script src="https://verifia.anybanq.lk/sdk/v1/anybanq.js"></script>
<script>
AnyBanQ.verify.open({
token: TOKEN_FROM_YOUR_SERVER,
onComplete: function (r) {
// r.status: APPROVED | REJECTED | FAILED | EXPIRED | MANUAL_REVIEW
// r.approved: true only for APPROVED
},
onClose: function (r) {},
});
</script>// Sessions of a workflow, newest first, each with its status.
const sessions = await pay.verifications.listSessions(WORKFLOW_ID);
const mine = sessions.find((s) => s.id === session.id);
// mine.status: PENDING | ACTIVE | COMPLETED | EXPIRED
// Raw: GET https://verifia.anybanq.lk/v1/workflows/$WORKFLOW_ID/sessionsWebhook events
- verification.completed
- verification.reviewed
verification.completed is sent once for each check in the workflow as it finishes, with verificationId, type, status, outcome and score. verification.reviewed follows when a reviewer decides a check that was held for manual review. There is no single event for the whole session; read the session's status as shown above.
2 · Collection
Collect from a local bank account
You create a payment link or a bill; the customer pays it on our hosted pay page by linking their bank account and approving the debit. HNB JustPay is the only live rail, so today the paying account must be an HNB account.
Amounts in this section are numbers in major units, in LKR: 25000 is LKR 25,000.00. That is what these endpoints take. Remittance, below, uses strings of minor units instead.
const link = await pay.collections.createPaymentLink({
description: "Portfolio top-up",
amount: 25000, // LKR 25,000.00: major units, as a number
customerName: "Nimal Perera",
customerPhone: "0771234567", // we text the link unless notify is false
expiresInDays: 7, // default 30
});
link.url; // send or show this to the customer
link.status; // unpaid | paid | expired | cancelled
// A payment link is a bill. Read it by the same id:
const bill = await pay.collections.getBill(link.id);
bill.status; // UNPAID | PAYING | HELD | PAID | OVERDUE | CANCELLED | SPLIT | REFUNDEDcurl -X POST https://verifia.anybanq.lk/v1/payment-links \
-H "Authorization: Bearer $ANYBANQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Portfolio top-up",
"amount": 25000,
"customerName": "Nimal Perera",
"customerPhone": "0771234567",
"expiresInDays": 7
}'
# { "id": "…", "ocr": "…", "payToken": "…", "url": "https://pay.anybanq.lk/pay/…",
# "amount": 25000, "currency": "LKR", "status": "unpaid", "expiresAt": "…", "paidAt": null }
curl https://verifia.anybanq.lk/v1/bills/$ID -H "Authorization: Bearer $ANYBANQ_API_KEY"// A bill names the payer and a due date.
const bill = await pay.collections.createBill({
description: "Advisory fee, October",
amount: 15000,
dueDate: "2026-11-01",
payerNic: "199012345678",
payerPhone: "0771234567",
channels: ["sms"],
});
// Raw: POST https://verifia.anybanq.lk/v1/bills with the same fields.A pay button on your page
For a fixed price, sign the product into a code once and put the code on a button. The code is safe to publish: the amount is inside it and signed, so editing the page cannot change what is charged.
// Once, on your server: sign a product into a code. The price is inside it.
const { code } = await pay.collections.createPayButtonCode({
description: "Portfolio top-up",
amount: 25000,
});
// Raw: POST https://verifia.anybanq.lk/v1/payment-links/button<script src="https://verifia.anybanq.lk/sdk/v1/anybanq.js"></script>
<!-- Any element. A click creates a fresh link and opens the hosted pay page. -->
<button data-verifia-pay="CODE_FROM_YOUR_SERVER">Top up LKR 25,000</button>Webhook events
- bill.created
- bill.paid
- bill.scheduled
- bill.split
- bill.installments.scheduled
- bill.autopay.stopped
- bill.proof_submitted
bill.paid is the one to act on. It carries billId, ocr, amount, currency, settledVia, paymentRef and paidAt. A payment link sends the same events, because it is a bill; billId is the link's id.
3 · Remittance
Take a remittance
Remittance runs in test mode. The sender pays on a test card page, no card is charged and no recipient is paid. Orders carry sandbox: true and finish with payout.status of DRY_RUN. Where the test card page is not switched on, creating an order answers 503: “No collection rail is available for remittance orders”.
A sender abroad pays by card; the recipient is paid in LKR. You price the transfer with a quote, turn the quote into an order, and hand the browser the order's checkout token.
The card is charged in your organisation's charge currency: USD unless it was changed in the console. sourceCurrency must be that currency; a quote in any other is refused with 400. If the sender's card is in another currency, pass it as displayCurrency and the quote also carries indicative: the total in that currency, as an estimate to show. It is never what is charged.
Amounts are strings of minor units in both directions: "10000" is 100.00. Keep them as strings or integers in your own code. The library refuses a JavaScript number here so an amount never passes through a float.
// 1. Price it. Amounts are strings of minor units: "10000" is USD 100.00.
const quote = await pay.remittance.createQuote({
sourceCurrency: "USD",
sendAmountMinor: "10000", // or receiveAmountMinor, in LKR cents. Not both.
});
// quote.total, quote.receive, quote.rate, quote.rateExpiresAt
// 2. Make the order before the quote expires.
const order = await pay.remittance.createOrder({
quoteId: quote.id,
externalRef: "transfer-1001", // your own id. A retry with it returns this order.
payee: {
kind: "ACCOUNT",
bankCode: "7083",
accountNumber: "0010223348219",
name: "Nimal Perera",
},
sender: { name: "Asha Perera", country: "GB", dob: "1990-04-12" },
});
// order.status: AWAITING_PAYMENT, or IN_REVIEW if screening held it
// 3. Give the page the checkout token and nothing else.
res.json({ token: order.checkoutToken });curl -X POST https://verifia.anybanq.lk/v1/remittance/quotes \
-H "Authorization: Bearer $ANYBANQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sourceCurrency": "USD", "sendAmountMinor": "10000" }'
# 201
# { "id": "…", "fixedSide": "SEND",
# "send": { "amountMinor": "10000", "currency": "USD" },
# "fee": { "amountMinor": "…", "currency": "USD" },
# "total": { "amountMinor": "…", "currency": "USD" },
# "receive": { "amountMinor": "…", "currency": "LKR" },
# "rate": "…", "rateExpiresAt": "…" }
curl -X POST https://verifia.anybanq.lk/v1/remittance/orders \
-H "Authorization: Bearer $ANYBANQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"quoteId": "QUOTE_ID",
"externalRef": "transfer-1001",
"payee": { "kind": "ACCOUNT", "bankCode": "7083",
"accountNumber": "0010223348219", "name": "Nimal Perera" },
"sender": { "name": "Asha Perera", "country": "GB", "dob": "1990-04-12" }
}'
# 201
# { "id": "…", "externalRef": "transfer-1001", "status": "AWAITING_PAYMENT",
# "sandbox": true, "checkoutToken": "…", "checkoutUrl": "https://verifia.anybanq.lk/remit/…", … }Who gets paid
kind: "ACCOUNT"
A Sri Lankan bank account you name: bankCode (the LankaPay code), accountNumber (5 to 20 digits) and name.
kind: "HANDLE"
A recipient’s remittance handle, sent as handle. We resolve it to the account its owner verified; send no account details.
kind: "FUND"
Your organisation’s own fund account, set in the console. Send nothing else. This is the one to use when customers are funding an account they hold with you.
The sender needs a name. country, nationality, dob, email and phone are optional, and a date of birth settles most borderline screening matches. If you verified the sender first, pass that session's id as verificationSessionId; it must be one of yours and completed.
<script src="https://verifia.anybanq.lk/sdk/v1/anybanq.js"></script>
<script>
AnyBanQ.remit.open({
token: CHECKOUT_TOKEN_FROM_YOUR_SERVER,
onStatus: function (r) {}, // r.status, r.orderId
onComplete: function (r) {
// r.status: COMPLETED | FAILED | EXPIRED | CANCELLED
// r.success: true only for COMPLETED
// A prompt to refresh, not proof of payment. Confirm on your server.
},
onError: function (e) {}, // e.code, e.message
});
</script>const latest = await pay.remittance.getOrder(order.id);
latest.status; // see the table below
latest.sandbox; // true: collected on the test card page, no money behind it
latest.payout.status; // DRY_RUN while real payouts are off
await pay.remittance.listOrders({ status: "COMPLETED", page: 1, pageSize: 50 });
await pay.remittance.cancelOrder(order.id); // only before the sender starts paying
// Raw:
// GET https://verifia.anybanq.lk/v1/remittance/orders/$ID
// GET https://verifia.anybanq.lk/v1/remittance/orders?status=COMPLETED&page=1&pageSize=50
// POST https://verifia.anybanq.lk/v1/remittance/orders/$ID/cancelStatuses and their events
| Status | Event | Meaning |
|---|---|---|
| IN_REVIEW | remittance.order.in_review | Screening held the sender or the payee. Someone on your team releases or rejects it in the console. |
| AWAITING_PAYMENT | remittance.order.awaiting_payment | Ready to be paid. Sent when a held order is released; a new clear order starts here with remittance.order.created. |
| PAYING | remittance.order.paying | The sender is on the card page. It can no longer be cancelled or expired. |
| COLLECTED | remittance.order.collected | The bank authorised the card payment. The money has not arrived yet: card payments settle in bulk, usually one to two working days later. |
| SETTLED | remittance.order.settled | The bank's settlement file listed the payment. By default the payout is instructed only now. An organisation set to pay on collection skips this status; the event is still sent when its settlement arrives. |
| PAYOUT_INSTRUCTED | remittance.order.payout_instructed | The instruction to pay the recipient has been made. |
| COMPLETED | remittance.order.completed | Final. Read payoutStatus: DRY_RUN means no bank was instructed. |
| FAILED | remittance.order.failed | Final. failureReason says why. |
| EXPIRED | remittance.order.expired | Final. The quoted rate lapsed before the order was paid. |
| CANCELLED | remittance.order.cancelled | Final. You cancelled it, or a held order was rejected. |
Every order also sends remittance.order.created when it is made. All ten events carry the same data: orderId, externalRef, status, sandbox, send, receive, payoutStatus, failureReason and occurredAt.
4 · Bulk payouts
Pay many accounts
An API key cannot approve a payout batch. Your system can post a batch and read it. Approving it takes a second person, signed in to the console, who is not whoever created it. The library has no approve method and the API answers 403 to a key that tries.
Real payouts are not live yet. While they are off, sending a batch records every row as DRY_RUN and moves nothing. Each batch says so itself: paymentsLive is false.
const batch = await pay.payouts.createBatch({
name: "October redemptions",
rows: [
{ payeeName: "Nimal Perera", bankCode: "7010", accountNumber: "0012345678",
amount: "85000.00", reference: "RED-1" },
{ payeeName: "Kamala Silva", bankCode: "7083", accountNumber: "0098765432",
amount: "120000.00", reference: "RED-2" },
],
});
batch.status; // AWAITING_APPROVAL
batch.fundingReference; // quote this on the bank transfer that funds the batch
batch.validCount; // rows that will be paid
batch.invalidCount; // rows left out, each with its reasons:
for (const row of batch.rows) {
if (row.status === "INVALID") console.log(row.rowNumber, row.issues);
}
const latest = await pay.payouts.getBatch(batch.id);
const recent = await pay.payouts.listBatches(); // latest 100, without rowscurl -X POST https://verifia.anybanq.lk/v1/payout-batches \
-H "Authorization: Bearer $ANYBANQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "October redemptions",
"rows": [
{ "payeeName": "Nimal Perera", "bankCode": "7010",
"accountNumber": "0012345678", "amount": "85000.00", "reference": "RED-1" }
]
}'
curl https://verifia.anybanq.lk/v1/payout-batches/$ID -H "Authorization: Bearer $ANYBANQ_API_KEY"
curl https://verifia.anybanq.lk/v1/payout-batches -H "Authorization: Bearer $ANYBANQ_API_KEY"A row that fails a check does not fail the call. It comes back with status: "INVALID" and its issues, and is left out of the total, so you fund only what is valid. There is no idempotency key: a second batch with the same payees, accounts and amounts is accepted and flagged with repeatOf, so check listBatches() before retrying a create that timed out.
What happens after you create it
Step 1 · API key or console
Create
Every row is checked and its payee name screened. The batch is AWAITING_APPROVAL. Event: payout_batch.created.
Step 2 · A second person, in the console
Approve
Never an API key, and never whoever created the batch. Events: payout_batch.approved, or payout_batch.rejected with a reason.
Step 3 · Your bank transfer
Fund
One transfer for the batch total, quoting fundingReference. When the bank confirms it: payout_batch.funded.
Step 4 · An approver, in the console
Send
Each ready row is instructed. When no row is left pending: payout_batch.completed, with counts of paid, failed, returned and notSent.
Batch statuses: AWAITING_APPROVAL, APPROVED, REJECTED, CANCELLED, PROCESSING, then PAID, PARTIALLY_PAID, FAILED or DRY_RUN. Row statuses: INVALID, READY, PROCESSING, PAID, FAILED, RETURNED, DRY_RUN.
Webhook events
- payout_batch.created
- payout_batch.approved
- payout_batch.rejected
- payout_batch.cancelled
- payout_batch.funded
- payout_batch.completed
- payout_batch.row.returned
Each carries batchId. payout_batch.row.returned is sent when the bank sends a paid row back, with rowNumber, payoutReference, amountLkr and reason.
Webhooks
Check the signature, then act
Add an endpoint in the console under Webhooks and choose its events. The secret, which starts whsec_, is shown once. An endpoint receives only the events it lists. Endpoints cannot be added, changed or removed with an API key.
An event about a test record is delivered like any other and carries test: true in data. A live record's payload has no test field at all. Remittance events carry sandbox: true instead. Check the flag before you ship goods or credit an account.
POST https://your-app.example/hooks/anybanq
content-type: application/json
x-divp-event: remittance.order.completed
x-divp-signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with your whsec_ secret>
x-divp-delivery-id: 0f2c6f0e-… (the same on every retry)
x-divp-attempt: 1
{
"event": "remittance.order.completed",
"data": {
"orderId": "…",
"externalRef": "transfer-1001",
"status": "COMPLETED",
"sandbox": true,
"send": { "amountMinor": "10000", "currency": "USD" },
"receive": { "amountMinor": "…", "currency": "LKR" },
"payoutStatus": "DRY_RUN",
"failureReason": null,
"occurredAt": "…"
},
"sentAt": "2026-10-10T08:00:00.000Z"
}The signature
- Header:
x-divp-signature, with the valuesha256=followed by 64 hex characters. - Algorithm: HMAC-SHA256, keyed with your webhook secret.
- What is signed: the raw request body, byte for byte. Nothing else. A body your framework parsed and you turned back into JSON will not match.
- There is no timestamp header. The time is
sentAtinside the signed body: the moment the event was queued. It is the same on every retry, so a retry has the same signature as the first attempt. - Replays:
x-divp-delivery-idis the same on every retry of one delivery. Store it and skip a delivery you have already handled.
import express from "express";
import { verifyWebhook, AnybanqWebhookError } from "./anybanq-pay.mjs";
const app = express();
// The raw bytes, not parsed JSON: the signature covers the body exactly as sent.
app.post("/hooks/anybanq", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.headers, process.env.ANYBANQ_WEBHOOK_SECRET);
} catch (err) {
if (err instanceof AnybanqWebhookError) return res.status(400).send(err.reason);
throw err;
}
// A retry resends the same delivery. Skip one you have already handled.
if (event.deliveryId && (await alreadyHandled(event.deliveryId))) return res.sendStatus(200);
switch (event.event) {
case "remittance.order.completed":
await markTransferDone(event.data.externalRef);
break;
case "bill.paid":
await markInvoicePaid(event.data.billId, event.data.amount);
break;
case "payout_batch.completed":
await recordBatch(event.data.batchId, event.data.status);
break;
case "verification.completed":
await recordCheck(event.data.verificationId, event.data.status);
break;
}
res.sendStatus(200);
});By default the library refuses a delivery whose sentAt is more than an hour old. That accepts every automatic retry. If you replay an older delivery from the console on purpose, pass { toleranceSeconds } as the fourth argument.
import crypto from "node:crypto";
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.ANYBANQ_WEBHOOK_SECRET)
.update(rawBody) // the exact bytes received
.digest("hex");
const given = req.headers["x-divp-signature"] ?? "";
const ok =
given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));A delivery that fails is retried after 10 seconds, 30 seconds, 2 minutes, 10 minutes and 30 minutes: six attempts over about 43 minutes. After that it waits in the console's delivery list for you to replay. Send a webhook.test event from the console to prove your endpoint before anything real depends on it.
Errors
Refused is not the same as unanswered
The API answers errors as JSON with statusCode, message and error. message is a sentence, or a list of sentences when several fields failed validation. The usual ones: 400 for a bad request, 401 for a bad key, 403 for a missing scope, a service that is off or an address not on your whitelist, 404 for an id that is not yours, 409 for a conflict such as a reused externalRef, 429 when you are over a rate limit.
AnybanqApiError
The API answered and refused. status, message, details, body, and requestId when a response carries an x-request-id header (ours do not today).
AnybanqOutcomeUnknownError
A create or cancel was sent and no answer came back. Not a failure: the work may have been done.
AnybanqConnectionError
A read got no answer. Nothing changed; try again.
AnybanqWebhookError
A webhook delivery failed verification. Answer 400 and do nothing else.
import { AnybanqApiError, AnybanqOutcomeUnknownError } from "./anybanq-pay.mjs";
try {
order = await pay.remittance.createOrder(input);
} catch (err) {
if (err instanceof AnybanqOutcomeUnknownError) {
// Sent, no answer. The order may exist. Same externalRef, so this is safe:
order = await pay.remittance.createOrder(input);
} else if (err instanceof AnybanqApiError) {
// The API refused it. err.status, err.message (the API's own words).
throw err;
}
}Test mode
What “test” means, exactly
There are two different things called test here. Keep them apart.
divp_test_… keys
- Work before your account is approved.
- Are exempt from your IP whitelist.
- Are counted separately in your usage figures.
- Make test records. A bill, a payment link, a subscription plan or a payout batch made with a test key carries
test: trueand is kept apart from your live records. A test key lists test records only; a live key and the console list live ones only. - Reach nobody and move nothing. No text or email leaves for a test record, no bank account can be linked or debited, and a test batch is never approved, funded or sent.
- Make sandbox remittance orders only, or none where the test card page is off.
Services in test mode
- Set by us for the whole platform, not by the key you use.
- Remittance: the order is marked
sandbox: trueand collected on a test card page. - Payouts: rows and remittance payouts are recorded as
DRY_RUN;paymentsLiveisfalseon a batch. - The same is true with a live key until we switch these on.
Verification works the same way. A check run with a test key, or a step of a hosted verification started with one, is a test record marked test: true. No provider is contacted, nobody is messaged and nothing is metered. A one-time code comes back in the response as devCode, a bank account step accepts 123456, and the last four digits of the NIC or account number choose the outcome. A test check tells you nothing about a real person.
What each thing does with a test key
Bill, payment link, pay button
Marked test: true. Nobody is texted: smsDelivery comes back { sent: false, suppressed: true } and the text is kept under GET /v1/notifications for the test key. The pay page says it is a test and offers a simulated payment only.
Subscription plan
Marked test: true. A subscriber signs up without linking an account. Each cycle raises a test bill; nothing is debited and nobody is messaged.
Payout batch
Marked test: true. Nobody can approve or fund it and it is never sent. POST /v1/payout-batches/:id/simulate completes it as DRY_RUN.
Remittance order
Always sandbox: true, on the test card page. Where that page is off the create call answers 403 and creates nothing. A HANDLE payee is refused, because handles belong to real people: use ACCOUNT or FUND.
Webhooks
Still sent, with test: true in data (remittance: sandbox: true).
Read the record instead of assuming: bill.test, batch.test, order.sandbox, order.payout.status, batch.paymentsLive and each row's status say what actually happened.
Pay a test bill without clicking
In the browser, the pay page of a test bill has one button: simulate the payment. For an automated test, make the same call. With your test key, POST /v1/bills/:id/pay does the same.
# A bill made with a divp_test_ key comes back marked, and nobody is texted:
# { "id": "…", "payToken": "…", "test": true,
# "smsDelivery": { "sent": false, "suppressed": true, "destination": "*******000", … } }
# Read the text that was not sent (test key):
curl https://verifia.anybanq.lk/v1/notifications -H "Authorization: Bearer $ANYBANQ_TEST_KEY"
# Pay it. No API key: the pay token is the credential. Answers 404 for a real bill.
curl -X POST https://verifia.anybanq.lk/v1/public/bills/$PAY_TOKEN/test-pay
# { "status": "PAID", "settledVia": "TEST", "test": true, … }
# and bill.paid is delivered with "test": true in data.# Run a test batch to its end. Test key only; nothing is approved, funded or sent.
curl -X POST https://verifia.anybanq.lk/v1/payout-batches/$BATCH_ID/simulate -H "Authorization: Bearer $ANYBANQ_TEST_KEY"
# { "status": "DRY_RUN", "test": true, "paymentsLive": false, "rows": [ { "status": "DRY_RUN", … } ] }
# and payout_batch.completed is delivered with "test": true in data.Finish a test remittance without clicking
In the browser, the test card page has an approve and a decline button. For an automated test, drive the same two calls the checkout makes.
# 1. Start the payment, as the checkout page does. No API key: the token is the credential.
curl -X POST https://verifia.anybanq.lk/v1/public/remittance/checkout/$CHECKOUT_TOKEN/pay
# 2. Decide it. Answers 404 unless the order has "sandbox": true.
curl -X POST https://verifia.anybanq.lk/v1/public/remittance/checkout/$CHECKOUT_TOKEN/mock/complete \
-H "Content-Type: application/json" \
-d '{ "outcome": "APPROVED" }' # or "DECLINED"
# { "status": "COMPLETED" }Limits and rules
What a key can and cannot do
An API key cannot approve, reject or send a payout batch.
POST /v1/payout-batches/:id/approve answers 403 to a key. Approval needs a signed-in person who did not create the batch.
An API key cannot change remittance fees or margin.
PUT /v1/remittance/settings answers 403 to a key: “Remittance settings can only be changed from the console”. The key that creates orders cannot reprice them.
An API key cannot create another API key.
Keys, the IP whitelist and the public key are managed in the console. Every key is refused, scoped or not: 403, “API keys and API security can only be managed from the console”.
An API key cannot manage webhooks.
Endpoints are added, changed and removed in the console. POST /v1/webhooks answers 403 to every key: “Webhooks can only be managed from the console”. A key that could repoint an endpoint could send your events somewhere else.
An API key cannot change your account.
Team members, roles, embed domains and other organisation settings answer 403 to every key. They are changed by a person signed in to the console.
A scoped key reaches only what its scopes cover.
This holds for every route, bills and payment links included. A key scoped to verifications cannot issue a bill or create a remittance order. A key with no scopes is not narrowed.
A test key makes test records and sees only those.
Bills, payment links, subscription plans and payout batches made with a divp_test_ key carry test: true, reach nobody and move nothing. A test key cannot read or change a live record, and a live key cannot read a test one.
A FUND payee is fixed.
payee: { kind: "FUND" } pays your organisation’s own fund account, set in the console. Sending a bank, account, name or handle with it is refused, not ignored.
One quote makes one order.
A quote holds its rate for five minutes. A used or expired quote is refused; ask for a new one.
A checkout token lasts 24 hours.
After that the hosted checkout answers as if the order did not exist. The order itself expires sooner, with its quote, if nobody has started paying.
A payout batch holds up to 500 rows.
One row can carry at most LKR 5,000,000. Creating a batch is limited to five calls a minute, because each call screens every name.
Lists are paged.
pageSize goes up to 100 for remittance orders, bills and payment links. GET /v1/payout-batches returns the latest 100 batches and takes no parameters.
A live key can be tied to your addresses.
If you set an IP whitelist under API keys, a live key used from anywhere else answers 403. Test keys are exempt.
Services and scopes for each area
A service must be on for your organisation, and a scoped key must hold the scope. When a service is off the API answers 403 with “The … service is not enabled for your account”. When a scope is missing it answers 403 with “API key is not scoped for: …”.
| Area | Service that must be on | Key scope |
|---|---|---|
| Verify a customer | The checks in your workflow | verifications |
| Collect from a bank account | Bill issued | bills |
| Take a remittance | Remittance orders (off until we switch it on) | remittance |
| Pay many accounts | Bulk payouts (off until we switch it on) | payouts |
| Register webhooks | Always on | No key. Console only. |
Remittance and bulk payouts are off for every organisation until we switch them on. Ask your AnyBanQ contact, and tell us which you need.
Next
Start with a test key.
Registering gives you a divp_test_ key straight away. The full endpoint reference is in the console once you can sign in; it is not published on the open web.
