SalyPaySalyPayDocs

https://api.salypay.com/api

Developer portal

Build with theSalyPay Business API

Create customers, issue virtual accounts, receive payments, send payouts, and verify signed webhooks from your trusted backend using server-only secret keys.

Business key

sk_test_ / sk_live_ — call /api/v1/* from a trusted backend only

Environments

Key prefix selects TEST or LIVE — do not send environment in bodies

Webhook secret

whsec_ — verify HMAC signatures on your webhook server

Before you start

Examples use placeholders like <SALYPAY_TEST_SECRET_KEY>. No real secrets are embedded in this page.

{
  "success": true,
  "message": "Operation successful",
  "data": {},
  "timestamp": "2026-07-27T12:00:00.000Z"
}
export type ApiSuccess<T> = {
  success: true;
  message: string;
  data: T;
  timestamp: string;
};

export type ApiError = {
  success: false;
  error: {
    code: string;
    message: string;
    details?: unknown;
    timestamp: string;
    path: string;
    requestId?: string;
  };
  statusCode: number;
};

export type ApiResult<T> = ApiSuccess<T> | ApiError;

Read application data from response.data. Do not assume errors share the success shape.

Quickstart

Executable top-to-bottom in TEST. Switch the header to LIVE to update key prefixes in featured examples — sandbox credit remains test-only.

1

Create a business account

Dashboard JWT

Sign in as a SalyPay user, then register a business with your dashboard JWT. The response includes a one-time test API key.

curl --request POST \
  --url https://api.salypay.com/api/business/register \
  --header 'Authorization: Bearer <DASHBOARD_JWT>' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Acme Limited",
    "legalName": "Acme Technologies Limited",
    "registrationNumber": "RC123456",
    "industry": "Technology",
    "country": "NG"
  }'
{
  "success": true,
  "message": "Operation successful",
  "data": {
    "business": {
      "id": "5a1c4fc8-f52a-4e19-8b7e-111111111111",
      "name": "Acme Limited",
      "status": "SANDBOX",
      "kycStatus": "PENDING",
      "liveEnabled": false
    },
    "testKey": "sk_test_<shown-once-secret>"
  },
  "timestamp": "2026-07-27T12:00:00.000Z"
}
  • Save the test key immediately — SalyPay cannot retrieve it later.
  • Keep the secret in component memory only while revealing; clear on dismiss.
2

Store the test key on your server

Business key

Configure your backend secret manager. Never use frontend-exposed prefixes.

SALYPAY_BASE_URL=https://api.salypay.com/api
SALYPAY_SECRET_KEY=sk_test_<secret>
  • Do not prefix with NEXT_PUBLIC_, VITE_, or any client-exposed convention.
3

Confirm the key identity

Business key

Call whoami before creating resources to catch the wrong business or environment.

curl --request GET \
  --url https://api.salypay.com/api/v1/whoami \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>'
{
  "success": true,
  "message": "Operation successful",
  "data": {
    "businessId": "5a1c4fc8-f52a-4e19-8b7e-111111111111",
    "environment": "TEST"
  },
  "timestamp": "2026-07-27T12:00:00.000Z"
}
4

Create a customer

Business key

Customers are end-users of your business. Include dateOfBirth when you plan LIVE KYC. Responses never include passwords, PINs, or tokens.

curl --request POST \
  --url https://api.salypay.com/api/v1/customers \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "firstName": "Samuel",
    "lastName": "Daniel",
    "email": "samuel@example.com",
    "phone": "+2349066656543",
    "dateOfBirth": "1995-08-31"
  }'
{
  "success": true,
  "message": "Operation successful",
  "data": {
    "id": "8a641613-9d21-4699-a087-333333333333",
    "firstName": "Samuel",
    "lastName": "Daniel",
    "email": "samuel@example.com",
    "phone": "+2349066656543",
    "dateOfBirth": "1995-08-31T00:00:00.000Z",
    "isSuspended": false,
    "kycStatus": "PENDING",
    "businessId": "5a1c4fc8-f52a-4e19-8b7e-111111111111",
    "environment": "TEST"
  },
  "timestamp": "2026-08-02T12:00:00.000Z"
}
  • dateOfBirth is optional on create but required on the customer before LIVE KYC.
  • Duplicate email or phone returns HTTP 409 DUPLICATE_CUSTOMER.
5

Create a virtual account

Business key

Issue a bank account for inbound deposits. The customer must be active with an NGN wallet.

curl --request POST \
  --url https://api.salypay.com/api/v1/customers/<CUSTOMER_ID>/virtual-accounts \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>'
{
  "success": true,
  "message": "Operation successful",
  "data": {
    "id": "a49af068-c415-4ea9-a823-444444444444",
    "walletId": "344f52ea-e048-47c1-9ebc-555555555555",
    "accountNumber": "0123456789",
    "accountName": "Ada Lovelace",
    "bankName": "Sandbox Test Bank",
    "bankCode": "999999",
    "provider": "SANDBOX",
    "reference": "sbx_va_example",
    "isActive": true,
    "status": "ACTIVE",
    "businessId": "5a1c4fc8-f52a-4e19-8b7e-111111111111",
    "environment": "TEST"
  },
  "timestamp": "2026-07-27T12:00:00.000Z"
}
6

Simulate an account credit

Business key

Test-key only. Credits the wallet, creates a DEPOSIT transaction, and queues funding webhooks.

curl --request POST \
  --url https://api.salypay.com/api/v1/sandbox/virtual-accounts/<VIRTUAL_ACCOUNT_ID>/credit \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{ "amount": 5000 }'
{
  "success": true,
  "message": "Operation successful",
  "data": {
    "transactionId": "58cef24c-cb27-408d-8310-666666666666",
    "amount": 5000,
    "customerId": "<CUSTOMER_ID>",
    "fee": {
      "currency": "NGN",
      "grossAmount": 5000,
      "inflowPercentage": 0.5,
      "inflowFee": 25,
      "netAmount": 4975
    }
  },
  "timestamp": "2026-08-04T12:00:00.000Z"
}
  • This endpoint rejects live keys and live virtual accounts.
  • Settlement receives netAmount (gross − inflow fee). amount remains the gross for compatibility.
7

Read the transaction

Business key

List and fetch transactions. The API key selects the environment — do not pass an environment query on platform routes.

curl --request GET \
  --url 'https://api.salypay.com/api/v1/transactions?page=1&limit=20&type=DEPOSIT&status=SUCCESS&customerId=<CUSTOMER_ID>' \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>'
{
  "success": true,
  "message": "Operation successful",
  "data": {
    "items": [
      {
        "id": "58cef24c-cb27-408d-8310-666666666666",
        "type": "DEPOSIT",
        "status": "SUCCESS",
        "amount": 5000,
        "fee": 25,
        "currency": "NGN",
        "provider": "SANDBOX",
        "environment": "TEST",
        "source": "FIAT",
        "direction": "INFLOW",
        "grossAmount": 5000,
        "netAmount": 4975,
        "beneficiaryAmount": null,
        "inflowFee": 25,
        "payoutFee": 0,
        "stampDuty": 0,
        "totalFee": 25,
        "totalDebit": 0,
        "attributedCustomer": {
          "id": "<CUSTOMER_ID>",
          "displayName": "Ada Lovelace"
        },
        "createdAt": "2026-07-27T12:00:00.000Z"
      }
    ],
    "pagination": { "page": 1, "limit": 20, "total": 1, "pages": 1 }
  },
  "timestamp": "2026-07-27T12:00:00.000Z"
}
  • Responses include additive settlement fields: direction, grossAmount, netAmount, fee parts, and attributedCustomer.
  • Legacy amount and fee remain for compatibility. Crypto transaction shapes are unchanged.
8

Receive the funding webhook

Dashboard JWT

Register an HTTPS endpoint with your dashboard JWT, store the signing secret on your webhook server, and verify HMAC-SHA256 on the raw body.

curl --request POST \
  --url https://api.salypay.com/api/business/webhooks \
  --header 'Authorization: Bearer <DASHBOARD_JWT>' \
  --header 'Content-Type: application/json' \
  --data '{
    "url": "https://merchant.example/webhooks/salypay",
    "environment": "TEST",
    "enabledEvents": ["virtual_account.credited"]
  }'
{
  "success": true,
  "message": "Operation successful",
  "data": {
    "id": "29071ec8-fdfd-4725-a728-777777777777",
    "environment": "TEST",
    "url": "https://merchant.example/webhooks/salypay",
    "signingSecret": "whsec_<shown-once-secret>",
    "enabledEvents": ["virtual_account.credited"],
    "status": "ACTIVE"
  },
  "timestamp": "2026-07-27T12:00:00.000Z"
}
  • Headers: X-Saly-Signature, X-Saly-Event, X-Saly-Delivery.
  • LIVE funding webhooks may be provider-dependent until the live provider bridge is confirmed.

Authentication and environments

Platform routes use a business secret key. The key prefix selects the environment — never send environment or businessId in /api/v1 request bodies.

SurfaceCredentialSafe caller
/api/v1/*sk_test_… / sk_live_…Trusted backend only
Outbound webhookswhsec_…Your webhook server
ExamplesTESTsk_test_

API-key management and rotation

Create and revoke keys from the SalyPay dashboard API Keys screen. There is no single key-rotate endpoint — rotate by creating a new key, deploying it, verifying with whoami, then revoking the old key.

  1. 1Create a new key in the same environment from the dashboard
  2. 2Store it in your server secret manager
  3. 3Deploy with the new key
  4. 4Call GET /api/v1/whoami
  5. 5Verify traffic uses the new key
  6. 6Revoke the old key in the dashboard
  7. 7Remove the old secret from your systems
GET/api/v1/whoamiBusiness key

Verify the API key, business, and environment.

Success · HTTP 200

  • sk_test_ keys resolve to TEST.
  • Do not pass an environment query — the key selects it.

Request

curl --request GET \
  --url https://api.salypay.com/api/v1/whoami \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": {
    "businessId": "5a1c4fc8-f52a-4e19-8b7e-111111111111",
    "environment": "TEST"
  },
  "timestamp": "2026-07-27T12:00:00.000Z"
}

Errors401403

Settlement balance

Read the business settlement wallet for the API key's environment with GET /api/v1/balance. Dashboard receiving numbers use GET/POST /api/business/settlement-account (not personal virtual accounts).

NeedDashboard endpoint
Read settlement receiving accountGET /api/business/settlement-account?environment=
Create settlement account (owner/admin)POST /api/business/settlement-account?environment=
GET/api/v1/balanceBusiness key

Read the business settlement wallet ledger and available balance for the API key's environment.

Success · HTTP 200

  • The API key determines business and environment — do not send environment in the body or query.
  • availableBalance is spendable for payouts; ledgerBalance is the posted settlement balance.
  • Dashboard equivalent: GET /api/business/balance?environment=TEST|LIVE (JWT).

Request

curl --request GET \
  --url https://api.salypay.com/api/v1/balance \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": {
    "environment": "TEST",
    "currency": "NGN",
    "ledgerBalance": 150000,
    "availableBalance": 125000
  },
  "timestamp": "2026-08-03T12:00:00.000Z"
}

Errors401403

Transaction fees

Each business has one NGN fee schedule shared by TEST and LIVE. If no schedule is set, every charge is zero. Fee amounts appear on payout, inflow, and BVN verification responses and related webhooks.

ChargeCalculationWhen collected
BVN verificationConfigured flat feeOnly after successful BVN verification
Payoutmin(amount × payout% / 100, payoutFeeCap)After payout confirmed successful
Stamp dutyConfigured flat fee per payoutAfter payout confirmed successful
InflowgrossAmount × inflow% / 100When a virtual-account credit is posted

Settlement semantics

  • Inflows credit gross − inflowFee to settlement.
  • Payouts reserve and debit amount + payoutFee + stampDuty.
  • customerId on a payout is attribution only.
  • FEE transactions (e.g. BVN) are excluded from gross payment volume.

Customers

Platform customers never return passwords, refresh tokens, PINs, or 2FA secrets. Set dateOfBirth on create or PATCH before LIVE KYC. Create the customer before minting a virtual account.

POST/api/v1/customersBusiness key

Create an end-user customer in the key's environment.

Success · HTTP 201

Parameters
NameInTypeRequiredDescription
firstNamebodystringYes1–100 characters
lastNamebodystringYes1–100 characters
emailbodystringNoValid email
phonebodystringNoPhone string (E.164 recommended)
dateOfBirthbodystringNoYYYY-MM-DD. Optional on create; required on the customer before LIVE KYC
  • Duplicate email or phone returns HTTP 409 with code DUPLICATE_CUSTOMER.
  • Set dateOfBirth here (or via PATCH) before calling LIVE KYC.

Request

curl --request POST \
  --url https://api.salypay.com/api/v1/customers \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "firstName": "Samuel",
    "lastName": "Daniel",
    "email": "samuel@example.com",
    "phone": "+2349066656543",
    "dateOfBirth": "1995-08-31"
  }'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": {
    "id": "<CUSTOMER_ID>",
    "firstName": "Samuel",
    "lastName": "Daniel",
    "email": "samuel@example.com",
    "phone": "+2349066656543",
    "dateOfBirth": "1995-08-31T00:00:00.000Z",
    "kycStatus": "PENDING",
    "environment": "TEST",
    "isSuspended": false
  },
  "timestamp": "2026-08-02T12:00:00.000Z"
}

Errors400401409

PATCH/api/v1/customers/{customerId}Business key

Update customer profile fields. Use to set dateOfBirth before LIVE KYC.

Success · HTTP 200

Parameters
NameInTypeRequiredDescription
customerIdpathuuidYesCustomer ID
firstNamebodystringNo1–100 characters
lastNamebodystringNo1–100 characters
emailbodystringNoValid email
phonebodystringNoPhone string
dateOfBirthbodystringNoYYYY-MM-DD — required before LIVE KYC
isSuspendedbodybooleanNoSuspend or reactivate the customer
  • Every body field is optional; send only what you need to change.
  • Confirm persistence with GET /api/v1/customers/{customerId}.

Request

curl --request PATCH \
  --url https://api.salypay.com/api/v1/customers/<CUSTOMER_ID> \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "dateOfBirth": "1995-08-31"
  }'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": {
    "id": "<CUSTOMER_ID>",
    "dateOfBirth": "1995-08-31T00:00:00.000Z",
    "kycStatus": "PENDING"
  },
  "timestamp": "2026-08-02T12:00:00.000Z"
}

Errors400401404409

GET/api/v1/customers/{customerId}/balanceBusiness key

Read a customer's ledger and available wallet balance in the key's environment.

Success · HTTP 200

Parameters
NameInTypeRequiredDescription
customerIdpathuuidYesCustomer ID from POST /api/v1/customers
  • Scoped to the API key's business and environment.
  • availableBalance is spendable; ledgerBalance is the book balance (may include holds).
  • Amounts are in the named currency major units (e.g. NGN naira).

Request

curl --request GET \
  --url https://api.salypay.com/api/v1/customers/<CUSTOMER_ID>/balance \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": {
    "customerId": "<CUSTOMER_ID>",
    "currency": "NGN",
    "ledgerBalance": 5000,
    "availableBalance": 4500
  },
  "timestamp": "2026-08-02T12:00:00.000Z"
}

Errors401403404

Customer KYC

Verify a business customer's BVN or NIN with a secret key. Browser apps must call a trusted backend; never ship sk_* to the client. Use the customer ID returned by POST /api/v1/customers.

TEST · simulated BVN

The customer must belong to the same business and TEST environment as the API key. Otherwise the API returns 404 Customer not found.

BVNResult
00000000000VERIFIED, tier 2
11111111111REJECTED, tier 0
Any other valid valueVERIFIED, tier 1
POST/api/v1/customers/{customerId}/kycBusiness key

Verify a business customer's BVN or NIN from your app/backend. Not a dashboard action. Emits customer.kyc.updated.

Success · HTTP 201

Parameters
NameInTypeRequiredDescription
customerIdpathstringYesCustomer ID from POST /api/v1/customers
idempotencyKeybodystringYesNon-empty, max 128 characters. Generate a new value per logical KYC attempt.
idTypebodyBVN | NINYesAccepted values: BVN, NIN
idNumberbodystringYesCustomer BVN or NIN (5–30 characters)
  • TEST BVN verification is fully simulated — no real provider is called and no OTP is required.
  • 00000000000 → VERIFIED, tier 2 · 11111111111 → REJECTED, tier 0 · any other valid value → VERIFIED, tier 1.
  • Customer must belong to the same business and TEST environment as the API key (else 404).
  • Treat REJECTED as a completed KYC result in data (HTTP success), not only as an HTTP error.
  • Successful BVN verification collects the configured BVN flat fee from settlement. NIN is currently zero fee. Rejections release the reservation.
  • Insufficient settlement balance for the BVN fee returns HTTP 400 before the provider call.

Request

curl --request POST \
  --url https://api.salypay.com/api/v1/customers/<CUSTOMER_ID>/kyc \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "idempotencyKey": "kyc-20260804-customer-001",
    "idType": "BVN",
    "idNumber": "00000000000"
  }'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": {
    "customerId": "add36e49-bc2d-4e64-82f3-e1b159d62317",
    "status": "VERIFIED",
    "tier": 1,
    "fee": {
      "currency": "NGN",
      "quotedFee": 100,
      "bvnVerificationFee": 100,
      "feeTransactionId": "c590a32f-80d3-4d97-a646-fca24e13f042"
    }
  },
  "timestamp": "2026-08-04T12:00:00.000Z"
}

Errors400401403404

NeedEndpointCredential
Customer KYCPOST /api/v1/customers/{id}/kycBusiness key
Settlement balanceGET /api/v1/balanceBusiness key
Customer balanceGET /api/v1/customers/{id}/balanceBusiness key
Customer VAPOST /api/v1/customers/{id}/virtual-accountsBusiness key
Sandbox creditPOST /api/v1/sandbox/virtual-accounts/{id}/creditTest key
Internal transferPOST /api/v1/transfersBusiness key
External payoutPOST /api/v1/payoutsBusiness key

LIVE note

LIVE uses the customer's stored firstName, lastName, and dateOfBirth with the submitted ID. Missing DOB returns CUSTOMER_DATE_OF_BIRTH_REQUIRED (HTTP 400) — set it via create or PATCH /api/v1/customers/{id} first. Identity mismatch still returns HTTP success with status: REJECTED.

Banks and name enquiry

Populate payout bank selectors from the authenticated bank list, then resolve the account name before confirmation. Submit that exact accountName on POST /api/v1/payouts.

GET/api/v1/banksBusiness key

List Nigerian banks for payouts and name enquiry. Requires a business API key.

Success · HTTP 200

Parameters
NameInTypeRequiredDescription
countryquerystringNoOnly NG is supported. Defaults to NG.
  • Formerly unauthenticated — callers must now send Authorization: Bearer sk_*.
  • Use the returned code for name enquiry and payout bankCode fields.
  • A general GET /api/banks?country=NG endpoint remains available separately (not the v1 surface).

Request

curl --request GET \
  --url 'https://api.salypay.com/api/v1/banks?country=NG' \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": [
    {
      "code": "058",
      "name": "Guaranty Trust Bank",
      "country": "NG",
      "currency": "NGN",
      "type": "COMMERCIAL",
      "active": true
    }
  ],
  "timestamp": "2026-07-31T12:00:00.000Z"
}

Errors400401403503

POST/api/v1/banks/name-enquiryBusiness key

Resolve the account name for a Nigerian bank account before payout confirmation.

Success · HTTP 200

Parameters
NameInTypeRequiredDescription
accountNumberbodystringYesExactly 10 digits
bankCodebodystringYes3–6 digit bank code from GET /api/v1/banks
  • TEST returns Sandbox Account <last-four> with provider SANDBOX — no live banking call.
  • Submit the returned accountName exactly in POST /api/v1/payouts (with optional customerId for attribution).
  • Do not automatically retry after timeouts — respect 429 rate limits.

Request

curl --request POST \
  --url https://api.salypay.com/api/v1/banks/name-enquiry \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "accountNumber": "0123456789",
    "bankCode": "058"
  }'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": {
    "accountNumber": "0123456789",
    "accountName": "Sandbox Account 6789",
    "bankCode": "058",
    "accountStatus": "ACTIVE",
    "provider": "SANDBOX"
  },
  "timestamp": "2026-07-31T12:00:00.000Z"
}

Errors400401403429

Payout flow

  1. GET /api/v1/banks?country=NG
  2. POST /api/v1/banks/name-enquiry
  3. Review screen shows resolved name
  4. POST /api/v1/payouts with that exact accountName, a unique idempotencyKey, optional customerId for attribution, and no PIN

Payouts

External NGN payouts debit the business settlement wallet for the beneficiary amount plus payout fee and stamp duty. Authenticate with sk_test_… or sk_live_…. idempotencyKey is required. Optional customerId is attribution only.

POST/api/v1/payoutsBusiness key

Send an external NGN payout from the business settlement wallet to a Nigerian bank account.

Success · HTTP 200

Parameters
NameInTypeRequiredDescription
idempotencyKeybodystringYesNon-empty, max 128 characters. Generate a new value per logical payout. Retries with the same key return the original operation.
customerIdbodystring (UUID)NoOptional attribution only. Must belong to this business and environment. Does not debit the customer wallet.
amountbodynumberYesBeneficiary amount, minimum 1. Currency is always NGN — do not send currency.
accountNumberbodystringYesExactly 10 digits
bankCodebodystringYesBank code from GET /api/v1/banks (e.g. 058)
accountNamebodystringYesBeneficiary name — submit the exact value from name enquiry
narrationbodystringNoOptional transfer narration
  • Use sk_test_… keys. The key selects the environment — do not send environment, businessId, pin, or currency.
  • Settlement must have at least fee.totalDebit (amount + payoutFee + stampDuty) available. The full amount is reserved before provider submission.
  • customerId is attribution: when supplied, senderId is set and the payout appears in that customer’s transaction history.
  • Idempotency is scoped to the authenticated business and TEST/LIVE environment. Reusing a key for a different payout returns IDEMPOTENCY_KEY_REUSED.
  • TEST typically returns status COMPLETED. Amount 9999 simulates FAILED and releases the reservation.
  • 404 when customerId does not belong to this business/environment. 403 for suspended businesses or LIVE access not enabled.

Request

curl --request POST \
  --url https://api.salypay.com/api/v1/payouts \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "idempotencyKey": "payout-20260804-0001",
    "customerId": "<CUSTOMER_ID>",
    "amount": 10000,
    "accountNumber": "0123456789",
    "bankCode": "058",
    "accountName": "John Doe",
    "narration": "Customer withdrawal"
  }'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": {
    "status": "COMPLETED",
    "transactionId": "<TRANSACTION_ID>",
    "fee": {
      "currency": "NGN",
      "payoutPercentage": 1.5,
      "payoutFeeCap": 2000,
      "payoutFee": 150,
      "stampDuty": 50,
      "totalFee": 200,
      "amount": 10000,
      "totalDebit": 10200
    }
  },
  "timestamp": "2026-08-04T12:00:00.000Z"
}

Errors400401403404

Customer history

When customerId is supplied, the transaction's senderId is set to that customer and the payout appears in customer-filtered transaction lists. Settlement sufficiency is still checked against the business wallet.

Webhook

LIVE payouts emit payout.completed after successful provider confirmation (or payout.failed after the reservation is released). Both include the fee breakdown.

{
  "type": "payout.completed",
  "data": {
    "transactionId": "<TRANSACTION_ID>",
    "customerId": "<CUSTOMER_ID>",
    "currency": "NGN",
    "payoutPercentage": 1.5,
    "payoutFeeCap": 2000,
    "payoutFee": 150,
    "stampDuty": 50,
    "totalFee": 200,
    "amount": 10000,
    "totalDebit": 10200
  }
}

Virtual accounts

Prefer the customer-scoped create endpoint in the quickstart. Legacy POST /api/v1/virtual-accounts is advanced/legacy.

POST/api/v1/customers/{customerId}/virtual-accountsBusiness key

Mint a virtual account for inbound deposits.

Success · HTTP 201

Parameters
NameInTypeRequiredDescription
customerIdpathuuidYesCustomer ID
  • Sandbox returns Sandbox Test Bank details.

Request

curl --request POST \
  --url https://api.salypay.com/api/v1/customers/<CUSTOMER_ID>/virtual-accounts \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>'

Response

{
  "success": true,
  "data": {
    "id": "<VIRTUAL_ACCOUNT_ID>",
    "accountNumber": "0123456789",
    "bankName": "Sandbox Test Bank",
    "isActive": true,
    "status": "ACTIVE",
    "environment": "TEST"
  },
  "timestamp": "2026-07-27T12:00:00.000Z"
}

Errors400401404

Transactions and insights

Use paginated transaction history for operational lists and insights for aggregates. Platform results are scoped by the key's business and environment. Fiat responses now include additive settlement fields (direction, grossAmount, netAmount, fee parts, and customer attribution).

NeedEndpoint
Paginated historyGET /api/v1/transactions
One transactionGET /api/v1/transactions/{id}
Aggregated metricsGET /api/v1/insights
FieldMeaning
directionINFLOW, OUTFLOW, TRANSFER, or null
grossAmountOriginal inflow or payout beneficiary amount
netAmountAmount credited to settlement after inflow fee
beneficiaryAmountAmount sent to the payout beneficiary
inflowFee / payoutFee / stampDutyFee components
totalFee / totalDebitAggregated fee and full payout debit
attributedCustomerPrimary customer tied to the transaction
GET/api/v1/whoamiBusiness key

Verify the API key, business, and environment.

Success · HTTP 200

  • sk_test_ keys resolve to TEST.
  • Do not pass an environment query — the key selects it.

Request

curl --request GET \
  --url https://api.salypay.com/api/v1/whoami \
  --header 'Authorization: Bearer <SALYPAY_TEST_SECRET_KEY>'

Response

{
  "success": true,
  "message": "Operation successful",
  "data": {
    "businessId": "5a1c4fc8-f52a-4e19-8b7e-111111111111",
    "environment": "TEST"
  },
  "timestamp": "2026-07-27T12:00:00.000Z"
}

Errors401403

Webhooks

Register an HTTPS endpoint, store the signing secret on your webhook server, and verify HMAC-SHA256 on the exact raw body. Deduplicate with X-Saly-Delivery.

Event payloads

{
  "id": "85870158-f068-4ad2-bb43-888888888888",
  "type": "virtual_account.credited",
  "businessId": "5a1c4fc8-f52a-4e19-8b7e-111111111111",
  "environment": "TEST",
  "data": {
    "virtualAccountId": "a49af068-c415-4ea9-a823-444444444444",
    "customerId": "73c75fd6-b855-4bf1-992d-7939e8ff17b2",
    "transactionId": "58cef24c-cb27-408d-8310-666666666666",
    "amount": 10000,
    "grossAmount": 10000,
    "inflowPercentage": 0.5,
    "inflowFee": 50,
    "netAmount": 9950
  },
  "createdAt": "2026-08-04T12:00:00.000Z"
}

Signature verification

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post(
  "/webhooks/salypay",
  express.raw({ type: "application/json" }),
  (request, response) => {
    const rawBody = request.body as Buffer;
    const receivedSignature = String(
      request.header("X-Saly-Signature") ?? "",
    );
    const expectedSignature = crypto
      .createHmac("sha256", process.env.SALYPAY_WEBHOOK_SECRET!)
      .update(rawBody)
      .digest("hex");

    const received = Buffer.from(receivedSignature, "hex");
    const expected = Buffer.from(expectedSignature, "hex");
    const signatureIsValid =
      received.length === expected.length &&
      crypto.timingSafeEqual(received, expected);

    if (!signatureIsValid) {
      return response.status(401).send("Invalid signature");
    }

    const event = JSON.parse(rawBody.toString("utf8"));
    const deliveryId = request.header("X-Saly-Delivery");

    // Deduplicate with deliveryId before applying state changes.
    void event;
    void deliveryId;

    return response.status(200).send("ok");
  },
);

Signing secret

Store whsec_… only on your webhook server. After rotating a secret, update your receiver immediately — the previous secret becomes invalid.

Sandbox test values

Test-only. Never describe these as live-provider behavior.

ScenarioTest valueResult
Create virtual accountCustomer-specific create endpointGenerated Sandbox Test Bank account
Credit accountPOST /v1/sandbox/virtual-accounts/{id}/creditGross credit, inflow fee, net settlement, funding event
Customer KYC verify00000000000VERIFIED, tier 2 (TEST only)
Customer KYC reject11111111111REJECTED, tier 0 (TEST only)
Other valid KYC IDAny other 5–30 character valueVERIFIED, tier 1 (TEST only)
Transfer / payout failureAmount 9999Recorded FAILED; no funds moved (TEST)

Go-live

Submission moves the business to review. A SalyPay administrator must approve before live keys authenticate.

SANDBOX → UNDER_REVIEW → LIVE

UNDER_REVIEW → REJECTED → UNDER_REVIEW · LIVE → SUSPENDED

  • Submit KYC and go-live from the SalyPay business dashboard
  • Test keys stay for sandbox; live keys use sk_live_
  • Create and store your live key after approval
  • Test and live resources are fully isolated
  • LIVE keys return 403 until the business is approved

Errors and pagination

Display error.message and optionally expose error.requestId for support. Pagination max limit is 100.

StatusCondition
400Invalid input, unsupported country, missing NGN wallet, suspended customer, or unresolvable account
401Missing, invalid, or revoked business key
403Business suspended or LIVE key used before live approval
404Customer does not belong to the key's business/environment
409Duplicate customer email or phone number
429Name-enquiry or request rate limit exceeded
503Bank list temporarily unavailable
{
  "success": false,
  "error": {
    "code": "BAD_REQUEST",
    "message": "Validation or business-rule message",
    "timestamp": "2026-07-27T12:00:00.000Z",
    "path": "/api/v1/customers",
    "requestId": "req_01J3EXAMPLE"
  },
  "statusCode": 400
}

Integration checklist

Apply these when adopting the 2026-07-31 Business Customer API changes (customers, KYC, banks, name enquiry).

  • Add Authorization: Bearer sk_test_… or sk_live_… to GET /api/v1/banks
  • Keep business secret keys on a trusted server — never in the browser
  • Use the customer ID from POST /api/v1/customers for KYC and virtual-account creation
  • Send dateOfBirth (YYYY-MM-DD) on create or PATCH before LIVE KYC
  • Confirm DOB with GET /api/v1/customers/{id} before calling KYC
  • Do not send businessId or environment in /api/v1 request bodies
  • Populate payout banks from GET /api/v1/banks
  • Run name enquiry before showing payout confirmation
  • Submit idempotencyKey + resolved accountName in POST /api/v1/payouts (optional customerId is attribution only)
  • Ensure settlement availableBalance covers fee.totalDebit before payouts
  • Send idempotencyKey on POST /api/v1/customers/{id}/kyc and handle BVN fee / insufficient balance
  • Read settlement funds with GET /api/v1/balance
  • Never debit or check a customer wallet on POST /api/v1/payouts — settlement wallet only
  • Subscribe to virtual_account.credit_reversed when reversal notifications are required
  • Accept fee fields on funding, payout, and KYC webhook payloads
  • Treat KYC HTTP 201 with status REJECTED as a completed identity result
  • Handle CUSTOMER_DATE_OF_BIRTH_REQUIRED (400) by patching DOB, then retry KYC
  • Handle transfer/payout FAILED, COMPLETED, and PROCESSING states
  • Retry money-moving POSTs only with the same idempotencyKey after timeouts

Complete endpoint reference

Platform routes authenticated with a business secret key. Full schemas live in the canonical API specification.

Business key · /api/v1/*

MethodPath
GET/api/v1/whoami
GET/api/v1/balance
POST/api/v1/customers
GET/api/v1/customers
GET/api/v1/customers/{customerId}
GET/api/v1/customers/{customerId}/balance
PATCH/api/v1/customers/{customerId}
DELETE/api/v1/customers/{customerId}
POST/api/v1/customers/{customerId}/kyc
POST/api/v1/customers/{customerId}/virtual-accounts
POST/api/v1/virtual-accounts
GET/api/v1/virtual-accounts
GET/api/v1/virtual-accounts/{virtualAccountId}
PATCH/api/v1/virtual-accounts/{virtualAccountId}
DELETE/api/v1/virtual-accounts/{virtualAccountId}
POST/api/v1/sandbox/virtual-accounts/{virtualAccountId}/credit
GET/api/v1/banks
POST/api/v1/banks/name-enquiry
POST/api/v1/transfers
POST/api/v1/payouts
GET/api/v1/transactions
GET/api/v1/transactions/{transactionId}
GET/api/v1/insights

Source of truth: business-dashboard-api-spec.md. When docs and contract differ, update from the canonical specification.