DojaPay
Collection API

Collect payments with the DojaPay API

Give each customer or order its own bank account number. When they transfer into it from any Nigerian bank app, we credit your DojaPay wallet and notify your server.

  1. Sign in with your public and secret key to get an access token.
  2. Create a virtual account for a customer (static, reusable) or for one order (dynamic, expires).
  3. Your customer transfers to that account number.
  4. We send an inflow.received webhook to your server and credit your wallet.
  5. Confirm the payment by its reference, then fulfil.
Every request
POST https://api.dojapay.com/api/v1/virtual-account/create
Authorization: Bearer <access token>
Accept: application/json
Content-Type: application/json

Prefer to explore in a tool?

Import the whole API into Postman, or point any OpenAPI tool at the spec.

Getting started

Quickstart

From nothing to a working integration in a few minutes. Everything here uses sandbox keys, so it only ever touches test data and no real money moves.

  1. Create a DojaPay account at dojapay.com. Your sandbox keys are under Developer on your dashboard; set your webhook URL on the same page.
  2. Put the keys in your environment, never in source code.
  3. Sign in and keep the access_token. It lasts 24 hours; reuse it.
  4. Create a test account with create-test. You get back an account number starting 99.
  5. Pay it with Simulate a payment. Your webhook URL receives inflow.received, just as it will for a real transfer.
  6. Handle the webhook and confirm each payment before you fulfil. See Handling notifications for a working receiver.
  7. Go live. When your integration works, email support@dojapay.com. Live keys are issued once your business verification is approved; then sign in with them and call create instead of create-test.
Keep your keys in the environment
export DOJAPAY_PUBLIC_KEY="your sandbox public key"
export DOJAPAY_SECRET_KEY="your sandbox secret key"
Sign in, and keep the access token
curl -X POST "https://api.dojapay.com/api/v1/auth/login" \
  -u "$DOJAPAY_PUBLIC_KEY:$DOJAPAY_SECRET_KEY" \
  -H "Accept: application/json"

export DOJAPAY_ACCESS_TOKEN="the data.access_token you got back"
Create a test account
curl -X POST "https://api.dojapay.com/api/v1/virtual-account/create-test" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "accountType": "STATIC",
    "bank": "9psb",
    "firstName": "Ada",
    "lastName": "Obi",
    "email": "ada@example.com",
    "bvn": "22123456789"
  }'
Pay it, with test money
curl -X POST "https://api.dojapay.com/api/v1/sandbox/simulate-payment" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{ "accountNumber": "the data.account.number you got back", "amount": 15000 }'

Getting started

Keys and environments

There are two kinds of key, sandbox and live, and one base URL for both. The key you sign in with decides which environment every request runs in.

  • Sandbox keys come with every new account and only ever touch test data: test accounts, simulated payments, test balances. No bank is involved and no money moves. See Testing in the sandbox.
  • Live keys are issued once your business verification is approved. Only they can call create.
  • Using the wrong kind of key for an endpoint returns HTTP 403 with code "11".
  • Keep your secret key on your server. Never put it in a mobile app, a web page or a public repository.

Check your credentials tells you which environment a token belongs to.

Getting started

Response format

Every response has the same envelope. Check success and code, not the message text, which can change. Values in the examples on this page are illustrative.

A validation failure puts the reason for each field in errors.

One exception. An error raised before your request reaches an endpoint (HTTP 429 rate limit, 404 unknown path, 405 wrong method) uses the same envelope, but code is "08" and message is an object, not a list. For these, act on the HTTP status.

Success
{
  "success": true,
  "code": "00",
  "message": [
    "Transaction status retrieved"
  ],
  "data": {
    "...": "..."
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}
Validation error, HTTP 422
{
  "success": false,
  "code": "04",
  "message": [
    "Validation failed"
  ],
  "data": null,
  "errors": {
    "email": [
      "The email field must be a valid email address."
    ]
  },
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}
Rate limited, HTTP 429
{
  "success": false,
  "code": "08",
  "message": {
    "code": 429,
    "message": "Too Many Requests."
  },
  "data": null,
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Authentication

Sign in

POST /auth/login
Basic auth: public and secret key 5 requests / minute

Exchange your public and secret key for an access token. Send the keys as HTTP Basic credentials and no body.

Send the access token on every other request as Authorization: Bearer <access token>. It lasts 24 hours; expires_in is in seconds. Signing in again while it is still valid returns the same token, so it is safe to call on start-up.

The limit here is 5 a minute. Keep the token and reuse it; do not sign in per request.

Errors

  • 401 · 02 The keys are wrong.
  • 403 · 11 The key or the merchant account is not active.
Request
curl -X POST "https://api.dojapay.com/api/v1/auth/login" \
  -u "$DOJAPAY_PUBLIC_KEY:$DOJAPAY_SECRET_KEY" \
  -H "Accept: application/json"
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Authentication successful"
  ],
  "data": {
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
    "refresh_token": "8b1f0c4e9a2d...",
    "token_type": "Bearer",
    "expires_in": 86400
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Authentication

Refresh a token

POST /auth/refresh
No token needed 5 requests / minute

Get a new access token without sending your secret key. Needs no access token, so it works after the old one has expired.

You get a new access token and a new refresh token. The old refresh token stops working at once, so store the new one. Refresh tokens last 30 days.

Body

  • refresh_token string required

    The refresh token from your last sign-in or refresh.

Errors

  • 401 · 02 The refresh token is unknown, already used, or older than 30 days.
  • 403 · 11 The key or the merchant account is not active.
  • 422 · 04 refresh_token is missing.
Request
curl -X POST "https://api.dojapay.com/api/v1/auth/refresh" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "8b1f0c4e9a2d..."
  }'
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Token refreshed successfully"
  ],
  "data": {
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
    "refresh_token": "5d0e7a91c3f4...",
    "token_type": "Bearer",
    "expires_in": 86400
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Authentication

Check your credentials

GET /auth/me
Bearer token 30 requests / minute

The merchant and key behind the current access token.

Request
curl "https://api.dojapay.com/api/v1/auth/me" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json"
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Authenticated"
  ],
  "data": {
    "merchant": {
      "business_name": "Acme Ltd",
      "contact_email": "dev@acme.ng",
      "status": "active"
    },
    "credential": {
      "environment": "sandbox",
      "status": "active",
      "ip_restricted": false,
      "token_expires_at": "2026-10-30T10:15:00+01:00"
    }
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Authentication

Sign out

POST /auth/logout
Bearer token 30 requests / minute

Invalidate the current access token immediately. Sign in again to get a new one.

Request
curl -X POST "https://api.dojapay.com/api/v1/auth/logout" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json"
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Token invalidated"
  ],
  "data": [],
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Virtual accounts

Create a virtual account

POST /virtual-account/create
Bearer token Live key only 60 requests / minute

Give a customer or an order its own bank account number. Payments into it are credited to your wallet and notified to your webhook URL.

Show your customer data.account.number and the bank, and keep the number against the order or customer in your own system: it is how you will recognise the payment when the notification arrives. An account.created webhook is also sent; you can ignore it.

The example is a dynamic account for one order of exactly ₦15,000, valid for 3 hours. For a static account, send "accountType": "STATIC" and leave out amountType, amount and expiryHours.

Body

  • accountType string required

    STATIC belongs to one customer and can be paid any number of times. DYNAMIC is for one order and expires.

    STATIC DYNAMIC
  • bank string required

    The issuing bank. Lowercase, exactly: "9PSB" is rejected.

    9psb
  • firstName string required

    Customer first name.

  • lastName string required

    Customer last name.

  • email string required

    Customer email address.

  • bvn string bvn or nin required

    Customer BVN, 11 digits. Send bvn or nin.

  • nin string bvn or nin required

    Customer NIN, 11 digits. Required when bvn is absent.

  • amountType string required for DYNAMIC

    What the account accepts. Required for DYNAMIC accounts.

    EXACT ANY HIGHEROREXACT LOWEROREXACT
  • amount number required for DYNAMIC

    In naira. Required for DYNAMIC accounts unless amountType is ANY.

  • expiryHours integer required for DYNAMIC

    How long the account accepts payment, in whole hours from 1 to 24. Required for DYNAMIC accounts.

Errors

  • 403 · 11 Signed in with a sandbox key. Use create-test.
  • 422 · 04 A field is missing or invalid; see errors.
Request
curl -X POST "https://api.dojapay.com/api/v1/virtual-account/create" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "accountType": "DYNAMIC",
    "amountType": "EXACT",
    "amount": 15000,
    "expiryHours": 3,
    "bank": "9psb",
    "firstName": "Ada",
    "lastName": "Obi",
    "email": "ada@example.com",
    "nin": "12345678901"
  }'
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Virtual account created/updated successfully"
  ],
  "data": {
    "event_type": "account.created",
    "account": {
      "name": "Ada Obi",
      "number": "5030012345",
      "type": "DYNAMIC",
      "bank": "9PSB",
      "expiry": {
        "date": "2026-10-29T13:15:00"
      }
    },
    "order": {
      "amount": 15000,
      "currency": "NGN",
      "description": "Acme Ltd virtual account request",
      "country": "NGA",
      "amounttype": "EXACT"
    },
    "customer": {
      "email": "ada@example.com",
      "nin": "12345678901"
    }
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Virtual accounts

Create a test account

POST /virtual-account/create-test
Bearer token Sandbox key only 60 requests / minute

The same request and response as create, with a sandbox key. You get a test account number starting 99: no bank is involved and no money can be paid into it. Pay it with Simulate a payment.

Body

  • accountType string required

    STATIC belongs to one customer and can be paid any number of times. DYNAMIC is for one order and expires.

    STATIC DYNAMIC
  • bank string required

    The issuing bank. Lowercase, exactly: "9PSB" is rejected.

    9psb
  • firstName string required

    Customer first name.

  • lastName string required

    Customer last name.

  • email string required

    Customer email address.

  • bvn string bvn or nin required

    Customer BVN, 11 digits. Send bvn or nin.

  • nin string bvn or nin required

    Customer NIN, 11 digits. Required when bvn is absent.

  • amountType string required for DYNAMIC

    What the account accepts. Required for DYNAMIC accounts.

    EXACT ANY HIGHEROREXACT LOWEROREXACT
  • amount number required for DYNAMIC

    In naira. Required for DYNAMIC accounts unless amountType is ANY.

  • expiryHours integer required for DYNAMIC

    How long the account accepts payment, in whole hours from 1 to 24. Required for DYNAMIC accounts.

Errors

  • 403 · 11 Signed in with a live key. Use create.
  • 422 · 04 A field is missing or invalid; see errors.
Request
curl -X POST "https://api.dojapay.com/api/v1/virtual-account/create-test" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "accountType": "DYNAMIC",
    "amountType": "EXACT",
    "amount": 15000,
    "expiryHours": 3,
    "bank": "9psb",
    "firstName": "Ada",
    "lastName": "Obi",
    "email": "ada@example.com",
    "nin": "12345678901"
  }'
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Virtual account created/updated successfully"
  ],
  "data": {
    "event_type": "account.created",
    "account": {
      "name": "Ada Obi",
      "number": "5030012345",
      "type": "DYNAMIC",
      "bank": "9PSB",
      "expiry": {
        "date": "2026-10-29T13:15:00"
      }
    },
    "order": {
      "amount": 15000,
      "currency": "NGN",
      "description": "Acme Ltd virtual account request",
      "country": "NGA",
      "amounttype": "EXACT"
    },
    "customer": {
      "email": "ada@example.com",
      "nin": "12345678901"
    }
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Virtual accounts

Get an account

GET /virtual-account/details/{accountNumber}
Bearer token 60 requests / minute

One of your virtual accounts, by its account number.

Path parameters

  • accountNumber string required

    The 10-digit account number.

Errors

  • 404 · 26 No account with that number belongs to you.
  • 422 · 04 The account number is not 10 characters.
Request
curl "https://api.dojapay.com/api/v1/virtual-account/details/5030012345" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json"
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Account details retrieved successfully"
  ],
  "data": {
    "account_number": "5030012345",
    "account_name": "Ada Obi",
    "bank_name": "9PSB",
    "status": "active",
    "created_at": "2026-10-29T10:15:00+01:00"
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Virtual accounts

List accounts

GET /virtual-account/list
Bearer token 60 requests / minute

Your virtual accounts, newest first.

Query parameters

  • status string optional

    Only accounts in this state.

    active inactive expired
  • bank string optional

    Only accounts at this bank, for example 9psb.

  • per_page integer optional

    Results per page, 1 to 100. Default 20.

  • page integer optional

    Page number. Default 1.

Request
curl "https://api.dojapay.com/api/v1/virtual-account/list?status=active&per_page=20" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json"
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Virtual accounts retrieved successfully"
  ],
  "data": {
    "data": [
      {
        "account_number": "5030012345",
        "account_name": "Ada Obi",
        "account_type": "DYNAMIC",
        "bank_code": "9psb",
        "provider": "9PSB",
        "status": "active",
        "reference_code": "...",
        "expires_at": "2026-10-29T13:15:00+01:00",
        "assigned_at": null,
        "created_at": "2026-10-29T10:15:00+01:00"
      }
    ],
    "meta": {
      "current_page": 1,
      "per_page": 20,
      "total": 1,
      "last_page": 1
    }
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Webhooks

Payment received

When a customer pays into one of your accounts, we send a POST with this body to the webhook URL in your developer settings.

FieldMeaning
reference The payment's reference. Use it to confirm the payment, and to recognise a notification you have already handled.
data.event_type inflow.received for a payment. Ignore types you do not handle.
data.account_number Which of your virtual accounts was paid. Match it to your customer or order.
data.amount What the customer sent, as a string with two decimals.
data.settlement_amount What is credited to your wallet after fees.
data.charge Total fee taken: fee_amount plus vat.
data.sender_* Who paid, as reported by their bank.
data.fee_rule, data.fee_breakdown How the fee was worked out.
data.environment Present, as sandbox, only on a simulated payment. Real payments do not carry it.
inflow.received
{
  "success": true,
  "code": "00",
  "message": [
    "Inflow received and processed successfully."
  ],
  "data": {
    "event_type": "inflow.received",
    "session_id": "100004261029101500123456789012",
    "account_name": "Ada Obi",
    "account_number": "5030012345",
    "bank_name": "9PSB",
    "bank_code": "9PSB",
    "sender_bank_code": "000014",
    "sender_bank_name": "Access Bank",
    "sender_account_number": "0123456789",
    "sender_account_name": "ADA OBI",
    "amount": "15000.00",
    "settlement_amount": 14919.37,
    "gross_amount": 15000,
    "fee_amount": 75,
    "charge": 80.63,
    "vat": 5.63,
    "fee_rule": {
      "transaction_type": "inflow_virtual_account",
      "charge_type": "percentage",
      "value": "0.50000000",
      "cap": null
    },
    "fee_breakdown": {
      "fee": 75,
      "vat": 5.63,
      "vat_rate": 0.075
    }
  },
  "reference": "DPY-20261029101500482913",
  "timestamp": "2026-10-29T10:15:02+01:00"
}

Webhooks

Handling notifications

  • Reply with any 2xx within 10 seconds. Do slow work after you have replied.
  • With no reply in time, or a 5xx, we try again: up to 5 attempts in total, about 30 seconds, then 1, 2 and 5 minutes apart. A 4xx is treated as final and not retried, so do not return one for a temporary problem on your side.
  • Because of retries, the same notification can arrive more than once. Record each reference you process and ignore repeats, or you may fulfil an order twice.
  • Ignore event types and fields you do not use; both may be added to.
  • Always confirm the payment before fulfilling. Anyone who learns your webhook URL can post to it, and confirming is what proves the payment is real.

By the time a notification reaches you, the payment's status already reads success. Compare its gross_amount with what the order expects.

Webhook receiver
import express from "express";

const app = express();
app.use(express.json());

app.post("/webhooks/dojapay", async (req, res) => {
  // Reply first: we wait 10 seconds at most.
  res.sendStatus(200);

  const event = req.body;
  if (event?.data?.event_type !== "inflow.received") return;

  // Never trust the notification itself. Confirm it.
  const check = await fetch(
    "https://api.dojapay.com/api/v1/transaction/status/" + encodeURIComponent(event.reference),
    {
      headers: {
        Authorization: `Bearer ${process.env.DOJAPAY_ACCESS_TOKEN}`,
        Accept: "application/json",
      },
    }
  );
  const payment = await check.json();

  if (payment.success && payment.data.status === "success") {
    // Records the reference; does nothing if it has seen it before.
    await fulfilOnce(
      event.reference,
      event.data.account_number,
      payment.data.gross_amount
    );
  }
});

Transactions

Confirm a payment

GET /transaction/status/{reference}
Bearer token 60 requests / minute

Look a payment up by its reference before you fulfil. It is confirmed when status is success.

Here amount is what you received after fees and gross_amount is what the customer sent.

Path parameters

  • reference string required

    The reference from the payment notification.

Errors

  • 404 · 06 Not one of your payments. Do not fulfil.
Request
curl "https://api.dojapay.com/api/v1/transaction/status/DPY-20261029101500482913" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json"
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Transaction status retrieved"
  ],
  "data": {
    "reference": "DPY-20261029101500482913",
    "status": "success",
    "settlement_status": "pending",
    "amount": 14919.37,
    "gross_amount": 15000,
    "currency": "NGN",
    "type": "credit",
    "channel": "virtual_account",
    "narration": "Virtual Account Inflow",
    "initiated_at": null,
    "completed_at": null
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Transactions

Get a transaction

GET /transaction/{reference}
Bearer token 60 requests / minute

Everything status returns, plus the environment and when the record was created.

Path parameters

  • reference string required

    The transaction reference.

Errors

  • 404 · 06 Not one of your transactions.
Request
curl "https://api.dojapay.com/api/v1/transaction/DPY-20261029101500482913" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json"
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Transaction retrieved successfully"
  ],
  "data": {
    "reference": "DPY-20261029101500482913",
    "status": "success",
    "settlement_status": "pending",
    "amount": 14919.37,
    "gross_amount": 15000,
    "currency": "NGN",
    "type": "credit",
    "channel": "virtual_account",
    "narration": "Virtual Account Inflow",
    "environment": "live",
    "initiated_at": null,
    "completed_at": null,
    "created_at": "2026-10-29T10:15:01+01:00"
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Transactions

List transactions

GET /transaction/list
Bearer token 60 requests / minute

Your transactions, newest first. Each payment is followed by its fee as a separate fee entry referenced <reference>-FEE; filter type=credit for payments only.

Query parameters

  • status string optional

    Only transactions in this state.

    success pending failed
  • type string optional

    credit for payments in, fee for the fee taken on each.

    credit debit fee
  • channel string optional

    Only transactions through this channel.

    virtual_account transfer card ussd
  • from string optional

    Created on or after this date, YYYY-MM-DD.

  • to string optional

    Created on or before this date, YYYY-MM-DD.

  • reference string optional

    Matches any part of a reference.

  • per_page integer optional

    Results per page, 1 to 100. Default 20.

  • page integer optional

    Page number. Default 1.

Errors

  • 422 · 04 A filter is invalid, for example to before from.
Request
curl "https://api.dojapay.com/api/v1/transaction/list?type=credit&from=2026-10-01&per_page=20" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json"
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Transactions retrieved successfully"
  ],
  "data": {
    "data": [
      {
        "reference": "DPY-20261029101500482913",
        "status": "success",
        "settlement_status": "pending",
        "amount": 14919.37,
        "gross_amount": 15000,
        "currency": "NGN",
        "type": "credit",
        "channel": "virtual_account",
        "narration": "Virtual Account Inflow",
        "initiated_at": null,
        "completed_at": null,
        "created_at": "2026-10-29T10:15:01+01:00"
      }
    ],
    "meta": {
      "current_page": 1,
      "per_page": 20,
      "total": 1,
      "last_page": 1
    }
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Transactions

Re-verify with the bank

POST /transaction/verify
Bearer token 60 requests / minute

Ask the bank to re-confirm a virtual account payment. On success, returns the payment details as the bank reported them. Confirm a payment is enough for most integrations.

Body

  • reference string required

    The payment reference.

Errors

  • 404 · 06 Not one of your payments, or the bank did not confirm it.
  • 422 · 04 The reference is missing or unknown.
  • 422 · 17 This payment cannot be re-verified with its bank.
Request
curl -X POST "https://api.dojapay.com/api/v1/transaction/verify" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "DPY-20261029101500482913"
  }'

Wallet

Get your balance

GET /wallet/balance
Bearer token 60 requests / minute

Your DojaPay wallet balance, in naira. With a sandbox key, the total of your simulated payments after fees, marked "environment": "sandbox"; your real balance is never shown or touched.

Request
curl "https://api.dojapay.com/api/v1/wallet/balance" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json"
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Wallet balance retrieved successfully"
  ],
  "data": {
    "balance": 254330.75,
    "currency": "NGN",
    "status": "active"
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Sandbox

Testing in the sandbox

Sign in with your sandbox key and everything runs against test data. Nothing reaches a bank, nothing touches your real wallet, and nothing you do in the sandbox appears in your real transactions or reports.

  • create-test gives you a test account. Its number starts with 99, so it can never be a real account.
  • Simulate a payment pays it. Your real fee rule is applied, so the amounts you see are the amounts you will get.
  • Status, the transaction list, verify and your balance all show the test results. Dynamic accounts enforce their amount rule and expiry, as the bank does.
  • Endpoints the sandbox does not simulate refuse a sandbox key with HTTP 403; they never fall through to a real operation.

Sandbox webhooks go to the same webhook URL as live ones. Tell them apart by data.environment, which is "sandbox", and by the reference, which starts SBX- instead of DPY-. Confirming a sandbox payment with a live token returns 404, which is one more reason to confirm every payment before fulfilling.

When your integration works, switch to your live key and call create instead of create-test. Everything else stays the same.

Sandbox

Simulate a payment

POST /sandbox/simulate-payment
Bearer token Sandbox key only 60 requests / minute

Pay one of your test accounts as if a customer had transferred to it. You get an inflow.received webhook, and the payment shows in transaction status, the transaction list and your sandbox balance, exactly as a real one would.

Body

  • accountNumber string required

    A test account number from create-test.

  • amount number required

    What the customer sends, in naira. Your real fee rule is applied to it.

  • senderName string optional

    Who paid, as the bank would report it. Default SANDBOX SENDER.

  • senderAccountNumber string optional

    The payer's account number, 10 digits.

  • senderBankName string optional

    The payer's bank. Default Sandbox Bank.

Errors

  • 403 · 11 Signed in with a live key. Real payments come from real transfers.
  • 404 · 26 No test account with that number belongs to you.
  • 422 · 25 The account has expired.
  • 422 · 28 The amount breaks the account's rule, for example not exactly the amount an EXACT account expects.
  • 422 · 04 A field is missing or invalid; see errors.
Request
curl -X POST "https://api.dojapay.com/api/v1/sandbox/simulate-payment" \
  -H "Authorization: Bearer $DOJAPAY_ACCESS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "accountNumber": "9904790966",
    "amount": 15000,
    "senderName": "ADA OBI"
  }'
Response
{
  "success": true,
  "code": "00",
  "message": [
    "Sandbox payment simulated. An inflow.received webhook has been queued to your webhook URL."
  ],
  "data": {
    "event_type": "inflow.received",
    "session_id": "999999261029101500482913",
    "account_name": "Ada Obi",
    "account_number": "9904790966",
    "bank_name": "9PSB",
    "bank_code": "9PSB",
    "sender_bank_code": "000000",
    "sender_bank_name": "Sandbox Bank",
    "sender_account_number": "0000000000",
    "sender_account_name": "ADA OBI",
    "amount": "15000.00",
    "settlement_amount": 14919.37,
    "gross_amount": 15000,
    "fee_amount": 75,
    "charge": 80.63,
    "vat": 5.63,
    "fee_rule": {
      "transaction_type": "inflow_virtual_account",
      "charge_type": "percentage",
      "value": "0.50000000",
      "cap": null
    },
    "fee_breakdown": {
      "fee": 75,
      "vat": 5.63,
      "vat_rate": 0.075
    },
    "environment": "sandbox",
    "reference": "SBX-20261029101500731846"
  },
  "errors": null,
  "reference": "DJP-20261029101500K7Q2M9XA",
  "timestamp": "2026-10-29T10:15:00+01:00"
}

Reference

Error codes

The codes you are most likely to meet. code is a string; compare it as one.

Code HTTP Meaning What to do
00 200 Success
01 401 Authorization header missing or not a bearer token Send Authorization: Bearer <access token>.
02 401 Invalid credentials or token Check your keys, then sign in again.
03 401 Access token has expired Refresh it, or sign in again.
04 422 Validation failed Read the errors field.
06 404 Not found Check the reference belongs to you.
08 500 System error Retry later. Quote the response reference to support.
11 403 Permission denied Wrong key for this endpoint (live or sandbox), an IP not on your allow list, or an inactive account.
17 422 Request not usable for this operation Read the message.
25 422 Virtual account has expired Create a new account for the order.
26 404 Virtual account not found Check the account number.
28 422 Payment breaks the account amount rule Send the amount the account expects.

Reference

Rate limits

EndpointsRequests per minute
auth/login, auth/refresh 5
auth/me, auth/logout 30
Virtual accounts, transactions, wallet 60

Over the limit you get HTTP 429 (with code "08", as described under Response format). Wait a minute and retry.

Reference

OpenAPI and Postman

The same API, machine-readable. Both are generated from the definitions behind this page, so they always match it.

  • Postman. Import the collection, set publicKey and secretKey in its variables, and send Sign in. It stores the tokens for every other request.
  • OpenAPI 3.0. For Swagger, Insomnia, or generating a client in your language.

Ready to integrate?

Create an account, take your sandbox keys, and build against test accounts first.

Get your API keys

© 2026 DojaPay · support@dojapay.com