https://api.bovillus.finance/v3 · Sandbox https://sandbox.api.bovillus.finance/v3
Bovillus Finance Developers
Payments infrastructure for receiving banks and institutions
Bovillus Finance is an executing settlement institution. This API lets receivers follow every payment we send them and confirm settlement, lets sending institutions advise inbound credits, and lets account holders request payouts and read balances and statements.
Principles
- Nothing is final until the receiver says so. Funds are reserved when we generate a payment and debited only on your
ACSCconfirmation. - Every write is signed and idempotent. A repeated
Idempotency-Keyreturns the first result; it never makes a second payment. - ISO 20022 throughout. Field names, status codes, reason codes, purpose codes and camt.053 statements follow the standard, so your systems can map us without translation.
- Money is a string.
"25000.00", never a floating-point number. - Formats are files, not networks. We deliver pacs.008, MT103 or JSON as you choose. Bovillus Finance is not a SWIFT member; MT messages are FIN-format files.
Delivery modes
We deliver each payment by the mode agreed at onboarding: API, SFTP, IP_TO_IP (API or SFTP inside a private tunnel), T2T, KTT or MANUAL. Whatever the mode, you confirm through this API, or Bovillus Finance records your confirmation with its evidence.
Onboarding
Access is granted per institution. Documentation is public; credentials are not.
- Agreement and profile. Sign the receiver document. We agree your delivery mode, message format, currencies, limits and the accounts your profile may use.
- Key pair and certificate request. Generate an EC P-256 key pair in your HSM or key store and send us a certificate signing request. Never send a private key.
openssl ecparam -name prime256v1 -genkey -noout -out client.key openssl req -new -key client.key -out client.csr \ -subj "/O=Your Institution Ltd/CN=payments-api/C=HK"
- We issue your client certificate,
client_idand partner code, through the agreed secure channel. - Register your signing key. Give us your public key or a JWKS URL and the
keyidyou will use in signatures. - Sandbox tests. Pass the go-live checklist below in the sandbox.
- Production. After written go-ahead from both operations teams, we enable your production profile and IP allow-list.
Go-live checklist
| # | Test | Pass when |
|---|---|---|
| 1 | Token with valid and invalid assertion | 200 with token; 401 INVALID_TOKEN |
| 2 | Signed write, then the same with a broken signature | 2xx; then 401 INVALID_SIGNATURE |
| 3 | Same Idempotency-Key twice, then with a changed body | Same result twice; then 409 IDEMPOTENCY_CONFLICT |
| 4 | Scenario REJECT_AC01 | Status RJCT, reason AC01 |
| 5 | Receiver confirmation ACSC | Payment ACSC, fee chargedAt set |
| 6 | Webhook received and its signature verified | Your endpoint replies 2xx; duplicate eventId ignored |
Quickstart
Get a token, then confirm a payment. Every call uses your client certificate.
1. Get an access token
curl https://sandbox.api.bovillus.finance/v3/oauth2/token \
--cert client.crt --key client.key \
-d grant_type=client_credentials \
-d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
-d client_assertion="$ASSERTION" \
-d scope="payments:read confirmations:write"
# 200
{ "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 300,
"scope": "payments:read confirmations:write" }
import fs from "node:fs";
import https from "node:https";
import { SignJWT, importPKCS8 } from "jose";
const TOKEN_URL = "https://sandbox.api.bovillus.finance/v3/oauth2/token";
const key = fs.readFileSync("client.key"); // PKCS#8 PEM
const agent = new https.Agent({ cert: fs.readFileSync("client.crt"), key });
const assertion = await new SignJWT({})
.setProtectedHeader({ alg: "ES256", kid: process.env.KEY_ID })
.setIssuer(process.env.CLIENT_ID).setSubject(process.env.CLIENT_ID)
.setAudience(TOKEN_URL).setJti(crypto.randomUUID())
.setIssuedAt().setExpirationTime("60s")
.sign(await importPKCS8(key.toString(), "ES256"));
const body = new URLSearchParams({
grant_type: "client_credentials",
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: assertion,
scope: "payments:read confirmations:write",
});
// use the same agent (same certificate) for every call with this token
import time, uuid, jwt, requests
TOKEN_URL = "https://sandbox.api.bovillus.finance/v3/oauth2/token"
CERT = ("client.crt", "client.key")
key = open("client.key").read()
assertion = jwt.encode(
{"iss": CLIENT_ID, "sub": CLIENT_ID, "aud": TOKEN_URL,
"jti": str(uuid.uuid4()), "iat": int(time.time()), "exp": int(time.time()) + 60},
key, algorithm="ES256", headers={"kid": KEY_ID})
r = requests.post(TOKEN_URL, cert=CERT, data={
"grant_type": "client_credentials",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": assertion,
"scope": "payments:read confirmations:write"})
token = r.json()["access_token"] # bound to client.crt, valid 300 s
2. Sign the request
Every write carries Content-Digest and an RFC 9421 signature over the method, URL, digest, idempotency key and request id. The signature base is exactly these lines:
"@method": POST
"@target-uri": https://sandbox.api.bovillus.finance/v3/payments/2b8f4c1a-…/receiver-confirmations
"content-digest": sha-256=:9Xq2…=:
"idempotency-key": skit-conf-0001-acsc
"x-request-id": 6f1c2d1e-8a1b-4b7e-9f0e-2b6f5c1d9a10
"@signature-params": ("@method" "@target-uri" "content-digest" "idempotency-key" "x-request-id");created=1794400000;keyid="partner-skit-2026-11";alg="ecdsa-p256-sha256"import { createHash, createPrivateKey, sign } from "node:crypto";
export function signedHeaders({ method, url, body, idemKey, requestId, keyId, pem }) {
const digest = "sha-256=:" + createHash("sha256").update(body).digest("base64") + ":";
const created = Math.floor(Date.now() / 1000);
const params = `("@method" "@target-uri" "content-digest" "idempotency-key" "x-request-id")` +
`;created=${created};keyid="${keyId}";alg="ecdsa-p256-sha256"`;
const base = [
`"@method": ${method}`, `"@target-uri": ${url}`, `"content-digest": ${digest}`,
`"idempotency-key": ${idemKey}`, `"x-request-id": ${requestId}`,
`"@signature-params": ${params}`].join("\n");
const sig = sign("sha256", Buffer.from(base),
{ key: createPrivateKey(pem), dsaEncoding: "ieee-p1363" }).toString("base64");
return { "Content-Digest": digest, "Idempotency-Key": idemKey, "X-Request-ID": requestId,
"Signature-Input": `sig1=${params}`, "Signature": `sig1=:${sig}:` };
}
import base64, hashlib, time
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
def signed_headers(method, url, body: bytes, idem_key, request_id, key_id, pem: bytes):
digest = "sha-256=:" + base64.b64encode(hashlib.sha256(body).digest()).decode() + ":"
params = ('("@method" "@target-uri" "content-digest" "idempotency-key" "x-request-id")'
f';created={int(time.time())};keyid="{key_id}";alg="ecdsa-p256-sha256"')
base = "\n".join([f'"@method": {method}', f'"@target-uri": {url}',
f'"content-digest": {digest}', f'"idempotency-key": {idem_key}',
f'"x-request-id": {request_id}', f'"@signature-params": {params}'])
key = serialization.load_pem_private_key(pem, password=None)
r, s = decode_dss_signature(key.sign(base.encode(), ec.ECDSA(hashes.SHA256())))
sig = base64.b64encode(r.to_bytes(32, "big") + s.to_bytes(32, "big")).decode()
return {"Content-Digest": digest, "Idempotency-Key": idem_key, "X-Request-ID": request_id,
"Signature-Input": f"sig1={params}", "Signature": f"sig1=:{sig}:"}
3. Confirm the payment
POST /v3/payments/2b8f4c1a-6d3e-4f7a-9c2b-1e5d8a7f3c90/receiver-confirmations
Authorization: Bearer eyJ…
Content-Type: application/json
…signed headers from step 2…
{
"result": "ACSC",
"receiverReference": "SKIT-CR-889102",
"settledAmount": { "currency": "USD", "value": "1000.00" },
"valueDate": "2026-11-12",
"confirmedAt": "2026-11-12T08:15:00Z"
}
# 201 → payment.status "ACSC", bovillusState "SETTLED_BY_RECEIVER", fee.chargedAt setSecurity profile
The API follows the OpenID Foundation FAPI 2.0 Security Profile, the profile open-banking regimes use for payment APIs.
| Layer | Control | Standard |
|---|---|---|
| Transport | TLS 1.2+ with your client certificate on every call | Mutual TLS |
| Client authentication | A JWT signed with your private key; we hold only your public key | OAuth 2.0 private_key_jwt, RFC 7523 |
| Access tokens | 300 seconds, scoped, bound to your certificate | RFC 8705 |
| Message integrity | Every write signed; body hashed; 300-second freshness window | RFC 9421, RFC 9530 |
| Duplicates | Idempotency-Key on every write, kept 30 days | IETF Idempotency-Key header |
| Our signatures | Webhooks and responses signed by Bovillus Finance; keys at /.well-known/jwks.json | RFC 9421, RFC 7517 |
| Network | Per-partner IP allow-list, rate limits, web application firewall | IETF RateLimit fields |
| Keys at Bovillus | Signing keys in AWS KMS; credentials in AWS Secrets Manager | FIPS 140-validated modules |
| Audit | Every request recorded with partner, certificate, address, time and result, append-only | - |
Scopes
| Scope | Allows |
|---|
Payment lifecycle
Statuses are ISO 20022 transaction status codes. bovillusState says the same thing in our words.
RCVDPDNGACTCACSPACSC| status | bovillusState | What has happened to the money |
|---|---|---|
RCVD | RECEIVED | Nothing yet |
PDNG | AWAITING_APPROVAL | Nothing yet; Bovillus Finance is reviewing the request |
ACTC | GENERATED | Amount and fee reserved; available balance reduced |
ACSP | DELIVERED / ACKNOWLEDGED | Still reserved; the receiver has the instruction |
ACSC | SETTLED_BY_RECEIVER | Debited, success fee charged, reservation closed. Final. |
RJCT / RTND / CANC | REJECTED / RETURNED / CANCELLED | Reservation released; nothing debited |
Reason codes
Rejections and returns carry an ISO 20022 ExternalStatusReason1Code, for example AC01 incorrect account, AC04 closed account, AM04 insufficient funds, RR04 regulatory reason.
For receiving banks and institutions
Receiving payments from Bovillus Finance
How a payment reaches you, what you return, and how you confirm settlement. You choose the delivery mode at onboarding; the API mode is described in full here.
1234The endpoint you provide
One HTTPS endpoint, for example POST https://api.yourbank.example/bovillus/payments, registered at onboarding with a test and a production URL. Allow-list our address 18.184.101.36.
| Item | What Bovillus Finance sends |
|---|---|
| Body | JSON instruction (application/json), or ISO 20022 pacs.008.001.08 (application/xml) validated against the XSD before sending. MT103 copies can be attached. |
| Authentication | Your choice: mutual TLS with the Bovillus client certificate, an OAuth 2.0 token we obtain from your token endpoint, or an API key header you name. |
Idempotency-Key | The instructionId. A repeat must return your first answer, never a second credit. |
X-Request-ID | Unique per attempt, for your logs and ours. |
| Signature | RFC 9421 Signature-Input and Signature with Content-Digest, made with our key. Verify with https://api.bovillus.finance/v3/.well-known/jwks.json. |
POST /bovillus/payments HTTP/1.1
Host: api.yourbank.example
Content-Type: application/json
Idempotency-Key: BOV-20261111-000123
X-Request-ID: 0d6e2c4a-9b1f-4f3e-8a7c-5e2d1b9f6a40
Content-Digest: sha-256=:…=:
Signature-Input: sig1=("@method" "@target-uri" "content-digest" "idempotency-key" "x-request-id");created=1794400000;keyid="bovillus-2026-11";alg="ecdsa-p256-sha256"
Signature: sig1=:…:
{
"instructionId": "BOV-20261111-000123",
"endToEndId": "SKIT-RED-0001",
"uetr": "8a4d2f6e-3b1c-4e9a-8f7d-2c5b1a9e6d30",
"paymentId": "2b8f4c1a-6d3e-4f7a-9c2b-1e5d8a7f3c90",
"amount": { "currency": "USD", "value": "1000.00" },
"debtor": { "name": "Bovillus Finance" },
"debtorAccount": { "accountNumber": "08122800051" },
"creditor": { "name": "SK Information Technologies" },
"creditorAccount": { "otherId": "SKIT-CIS-20260827" },
"purposeCode": "SUPP",
"remittanceInformation": "Redemption lot 1",
"confirmationUrl": "https://api.bovillus.finance/v3/payments/2b8f4c1a-…/receiver-confirmations",
"createdAt": "2026-11-11T09:30:00Z"
}What you return
| Your answer | Meaning | What Bovillus does |
|---|---|---|
202 with receiverReference, status ACSP | Accepted for processing | Marks DELIVERED / ACKNOWLEDGED; funds stay reserved |
200 same body as before | A repeat you already accepted | No change |
202 status ACSC | Already credited | Treated as your confirmation; debit and fee posted |
4xx with ISO reason code | Refused (final) | Marks RJCT; reservation released |
5xx, 429 or no answer in 30 s | Temporary | Retries with the same key, with back-off, for up to 24 hours |
HTTP/1.1 202 Accepted
Content-Type: application/json
{ "receiverReference": "SKIT-IN-55120", "status": "ACSP" }Confirming settlement
A 2xx means accepted, not credited. When the beneficiary is credited, rejected or the funds are returned, tell us in one of three ways:
- Confirmation API (recommended).
POST /v3/payments/{paymentId}/receiver-confirmationswithACSC,RJCTorRTND. See the Quickstart. - Status endpoint. Give us
GETon your side byinstructionId; we poll it until final. - Officer confirmation. For KTT, T2T and manual delivery, your officer confirms in writing with your reference; Bovillus Finance records it with the evidence.
Other delivery modes
| Mode | How it reaches you |
|---|---|
SFTP | JSON or pacs.008 file with a SHA-256 manifest uploaded to your path, key-based authentication; you return an acknowledgement file |
IP_TO_IP | The API or SFTP mode inside a private tunnel between fixed addresses |
T2T | Raw TCP session to your listener; no web proxy or CDN in front of it |
KTT | Tested telex from 210616 BOVILS G with the agreed test key |
MANUAL | Verification session link and access code with the institutional PDF and package files |
Webhooks
Subscribe once; we push each status change to your HTTPS endpoint.
- Events:
payment.status_changed,payment_request.status_changed,inbound_transfer.status_changed. - Each delivery is signed with RFC 9421 by Bovillus Finance, with
Content-Digest. Verify against the key named bykeyidat/.well-known/jwks.json, and refuse anything older than 300 seconds. - Reply 2xx within 10 seconds, then process. We retry for 72 hours with exponential back-off.
- Delivery is at least once: keep each
eventIdand ignore repeats. - A webhook tells you something changed;
GET /payments/{paymentId}is the record.
{
"eventId": "c1f7a0e2-5b3d-4c8e-9a6f-0d2b7e4c1a95",
"type": "payment.settled",
"occurredAt": "2026-11-12T08:15:03Z",
"data": { "paymentId": "2b8f4c1a-…", "status": "ACSC", "bovillusState": "SETTLED_BY_RECEIVER", … }
}Sandbox
The same contract at https://sandbox.api.bovillus.finance/v3, with no balance movement and no external delivery.
Set the Bovillus-Sandbox-Scenario header on a payment request or inbound transfer to force an outcome.
| Scenario | Result |
|---|---|
SUCCESS | Moves through to ACSC with webhooks at each step |
REJECT_AC01 | RJCT, reason AC01 incorrect account |
REJECT_AM04 | RJCT, reason AM04 insufficient funds |
RETURN_AC04 | ACSC, then RTND with reason AC04 |
DELAY_ACK | Stays ACSP for 15 minutes before acknowledgement |
TIMEOUT | 503 SERVICE_UNAVAILABLE; retry with the same key succeeds |
DUPLICATE | Returns the stored first result, as a real repeat would |
Errors
Every error is an RFC 9457 problem document with a stable code, your requestId, and whether a retry is safe.
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{ "type": "https://bovillus.finance/developers/errors/IDEMPOTENCY_CONFLICT",
"title": "The Idempotency-Key was used with a different body",
"status": 409, "code": "IDEMPOTENCY_CONFLICT",
"requestId": "6f1c2d1e-8a1b-4b7e-9f0e-2b6f5c1d9a10", "retryable": false }| code | HTTP | Retry | What to do |
|---|
Retry only when retryable is true, with the same Idempotency-Key. A 5xx never changes anything.
Changelog
3.0.0 · 11 November 2026
- New interface under
/v3: FAPI 2.0 security profile, signed writes, ISO 20022 statuses and fields. - Receiver confirmations replace the partner-specific settlement callback.
- Partner-initiated payouts are now payment requests, approved by Bovillus Finance before funds move.
- Accounts, camt.053 statements and webhooks added.
2.2 · deprecated
POST /v1/transfersis retired and returns 410./api/incoming/transfer,/api/settlement/callback,/api/v2/*and/api/canonical/*stay available to existing partners until each has moved to v3, and close 180 days after that partner's notice.
Download the contract
OpenAPI 3.1, the single source for this portal. Copy it, or ask Bovillus Finance for the file.
