Merchant API Documentation

Integrate Hanypay to collect payments, send payouts, issue refunds, and manage transactions programmatically.

Base URLhttps://payment-ms.hanypay.co/api/v1/merchant

Getting Started

The Hanypay Merchant API lets you accept AKL payments from customers, send payouts, issue refunds, and query transaction history. All communication is over HTTPS and authenticated via API keys.

Quick start

  1. Create a business in the Hanypay Dashboard
  2. Go to Business → API Keys and create an API key
  3. Copy the key — it is shown only once
  4. Pass the key in the Hanypay-Api-Key header on every request

Authentication

Include your API key in every request using the Hanypay-Api-Key header.

Example request
curl https://payment-ms.hanypay.co/api/v1/merchant/charges/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{"amount": "100.00"}'

All responses follow a consistent shape:

Success response
{
  "success": true,
  "message": "...",
  "data": { ... }
}
Error response
{
  "success": false,
  "error": "error_code",
  "message": "Human-readable error message"
}
Keep your API key secret. Do not expose it in client-side code, public repositories, or browser requests. If compromised, revoke and regenerate it immediately from the Dashboard.

Users

POST

Create User

POST /merchant/signup/

Create a new Hanypay user on behalf of your platform. The user gets a wallet automatically and can be used for subsequent payment collections and payouts.

If the email already belongs to an account your platform created, the response returns that user's details with is_new: false instead of an error, so re-running signup for one of your own customers is safe and idempotent.
If it belongs to a Hanypay account your platform did not create, you get 409 account_exists and nothing else. An API key identifies the calling platform, not the person, so account details are not handed out for customers you have no relationship with. Send that user through Sign In instead — they prove control of the address with a one-time code, and you receive tokens and their identifiers for the existing account.
This response is not a sign-in. An API key proves which platform is calling, not who the person is — anyone can submit any email address, so treating “user already exists” as successful authentication would let a caller into an account that is not theirs. To sign a returning user in, send them through Sign In: they prove control of the address with a one-time code and you receive tokens for their existing account.
This endpoint uses the Account service base URL: https://account-ms.hanypay.co/apikeys
FieldTypeRequiredDescription
emailstringRequiredUser's email address
phonestringRequiredUser's phone number (8-15 digits)
first_namestringRequiredUser's first name
last_namestringRequiredUser's last name
countrystringRequiredCountry code (e.g. "GH"), country name (e.g. "Ghana"), or country UUID
curl
curl -X POST https://account-ms.hanypay.co/apikeys/merchant/signup/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "email": "customer@example.com",
    "phone": "0241234567",
    "first_name": "John",
    "last_name": "Doe",
    "country": "GH"
  }'
Response — new user (201)
{
  "status": true,
  "message": "User created successfully.",
  "data": {
    "user_id": "a1b2c3d4-...",
    "email": "customer@example.com",
    "wallet_number": "1234567890123456",
    "is_new": true
  }
}
Response — existing user your platform created (200)
{
  "status": true,
  "message": "User already exists.",
  "data": {
    "user_id": "a1b2c3d4-...",
    "email": "customer@example.com",
    "wallet_number": "1234567890123456",
    "is_new": false
  }
}

wallet_number is null if the user has no wallet yet.

Response — existing Hanypay account created elsewhere (409)
{
  "status": false,
  "error": "account_exists",
  "message": "An account already exists for this email. Ask the user to sign in with their Hanypay account instead."
}

Treat this as “route the user to sign-in”, not as a failure. Use Sign In to act for them, and Consent to see their balance.

Rate limit: 30 requests per minute per API key — exceeding it returns 429. Error codes: invalid_api_key, invalid_country, phone_taken, account_exists, creation_failed

POST

User Lookup

POST /merchant/user-lookup/

Resolve an email address to a Hanypay user id.

Requires the User Lookup capability, granted to your business by Hanypay. Until it is, every request returns 403 user_lookup_not_enabled whatever address you send.
FieldTypeRequiredDescription
emailstringRequiredMatched case-insensitively; surrounding spaces ignored
curl
curl -X POST https://account-ms.hanypay.co/apikeys/merchant/user-lookup/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{ "email": "customer@example.com" }'
Response (200)
{
  "status": true,
  "data": {
    "user_id": "a1b2c3d4-...",
    "email": "customer@example.com"
  }
}
This identifies a user. It does not authorise you to act for one. Anyone holding an API key can submit any address, so a successful lookup is not a sign-in and is not evidence the person is present. Use Sign In to act for them.

Every lookup is recorded with the address resolved and the business that asked, and lookups of users your platform did not onboard are flagged for review. Rate limit: 30 requests per minute per API key. Error codes: invalid_request, user_lookup_not_enabled (403), user_not_found (404)

Sign In

Let an existing Hanypay user sign in to your platform. Hanypay emails them a one-time code; you relay the code back and receive Hanypay tokens for that user. Your platform never sees their Hanypay password.

POST

Request Sign-in Code

POST /partner/login/request/

Sends an 8-digit code to the address, valid for 10 minutes. A new request replaces any previous code.

The response is identical whether or not the email belongs to a Hanypay account. Do not branch on it to decide whether a user exists — that would turn your integration into an account-existence oracle. Show the code-entry screen either way.
FieldTypeRequiredDescription
emailstringRequiredThe user's Hanypay email address
curl
curl -X POST https://account-ms.hanypay.co/apikeys/partner/login/request/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "email": "customer@example.com"
  }'
Response (200)
{
  "status": true,
  "message": "If that email belongs to a Hanypay account, a sign-in code has been sent."
}

Rate limit: 10 requests per minute per API key. Error codes: invalid_request, delivery_failed (503 — the code could not be emailed; ask the user to try again)

POST

Verify Sign-in Code

POST /partner/login/verify/

Exchanges the code for Hanypay tokens. The code is single-use and is discarded after 5 failed attempts, at which point the user must request a new one.

FieldTypeRequiredDescription
emailstringRequiredThe same email the code was sent to
otpstringRequiredThe 8-digit code from the email
curl
curl -X POST https://account-ms.hanypay.co/apikeys/partner/login/verify/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "email": "customer@example.com",
    "otp": "12345678"
  }'
Response (200)
{
  "status": true,
  "access": "eyJhbGciOi...",
  "refresh": "eyJhbGciOi...",
  "user_id": "a1b2c3d4-...",
  "email": "customer@example.com",
  "first_name": "John",
  "last_name": "Doe",
  "is_kyc_verified": true,
  "partner": {
    "business_id": "b2c3d4e5-...",
    "name": "Your Platform"
  }
}
Signing a user in does not grant your platform access to their wallet. Balance access is a separate, revocable grant — see Consent.

Rate limit: 10 requests per minute per API key. Error codes: invalid_request, invalid_code, code_expired

POST

Password Sign-in

POST /partner/login/password/

Sign a Hanypay user in with their Hanypay password, without the emailed code. Available to whitelabel platforms Hanypay operates, where asking a user to verify the same email twice adds friction without adding safety.

Requires the Password Login capability, granted separately from Sign In. If your platform is not one Hanypay operates, use Sign In instead — there the password never leaves the Hanypay–user relationship.
FieldTypeRequiredDescription
emailstringRequiredThe user's Hanypay email address
passwordstringRequiredTheir Hanypay password. Handle it in transit only — never store or log it
curl
curl -X POST https://account-ms.hanypay.co/apikeys/partner/login/password/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "email": "customer@example.com",
    "password": "their-hanypay-password"
  }'
Response — verified account (200)
{
  "status": true,
  "access": "eyJhbGciOi...",
  "refresh": "eyJhbGciOi...",
  "user_id": "a1b2c3d4-...",
  "email": "customer@example.com",
  "first_name": "John",
  "last_name": "Doe",
  "is_kyc_verified": true,
  "partner": { "business_id": "b2c3d4e5-...", "name": "Your Platform" }
}
Response — email not yet verified (200)
{
  "status": true,
  "otp_required": true,
  "code_sent": true,
  "message": "Enter the code sent to your email to finish signing in."
}
An address can be sent three sign-in codes per hour, and a code lasts ten minutes. Past that you still get otp_required — so keep showing your code screen — but with code_sent: false and error: "email_quota_exceeded". Read code_sent before telling the user to check their inbox, or they will wait for a message that is not coming.

On otp_required, show the code screen and post the code to Verify Sign-in Code. That completes the sign-in, returns the same tokens, and marks the address verified, so the user is not asked again.

A wrong password, an unknown address and a refused account all return the same 401 invalid_credentials. Do not use the response to work out whether an account exists.

Staff accounts and suspended, blocked or deleted accounts are refused. Rate limit: 10 requests per minute per API key. Error codes: invalid_request, invalid_credentials (401), password_login_not_enabled (403), delivery_failed (503)

Consent

Request a user's consent to view their wallet balance. The user receives an OTP via email which must be verified to grant access.

This endpoint uses the Account service base URL: https://account-ms.hanypay.co/apikeys. You can only request consent for users your business created via the Create User endpoint.
FieldTypeRequiredDescription
emailstringOptionalUser's email address (provide email or user_id)
user_iduuidOptionalUser ID from signup response (provide email or user_id)
curl
curl -X POST https://account-ms.hanypay.co/apikeys/merchant/consent/request/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{ "email": "customer@example.com" }'
Response (200)
{
  "status": true,
  "message": "Consent code sent to user."
}

Submit the consent OTP code to grant your business access to the user's wallet balance. Consent is permanent until explicitly revoked.

FieldTypeRequiredDescription
emailstringOptionalUser's email address (provide email or user_id)
user_iduuidOptionalUser ID (provide email or user_id)
codestringRequired8-digit OTP code from the user
curl
curl -X POST https://account-ms.hanypay.co/apikeys/merchant/consent/verify/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "email": "customer@example.com",
    "code": "12345678"
  }'
Response (200)
{
  "status": true,
  "message": "Consent granted.",
  "data": {
    "user_id": "a1b2c3d4-...",
    "consent_id": "e5f6g7h8-...",
    "wallets": [
      {
        "wallet_number": "1234567890123456",
        "currency_code": "AKL",
        "balance": "500.000000"
      }
    ]
  }
}

The user's wallets are returned with the grant, exactly as Get Balance would return them a moment later — the consent covers balance reads, so nothing is disclosed here that you could not then fetch. Treat the balances as a snapshot at the time of consent; call Get Balance for a current figure.

Rate limit: 5 requests per minute per API key. Error codes: invalid_user, invalid_code, code_expired

Revoke a previously granted consent. After revocation, balance queries for this user will be denied until consent is granted again.

FieldTypeRequiredDescription
emailstringOptionalUser's email address (provide email or user_id)
user_iduuidOptionalUser ID (provide email or user_id)
curl
curl -X POST https://account-ms.hanypay.co/apikeys/merchant/consent/revoke/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{ "email": "customer@example.com" }'
Response (200)
{
  "status": true,
  "message": "Consent revoked."
}

Wallets

GET

Get Balance

GET /merchant/user/{user_id}/balance/

Retrieve the wallet balances of a user your business created. Requires active consent from the user (see Consent endpoints above).

This endpoint uses the Account service base URL: https://account-ms.hanypay.co/apikeys
curl
curl https://account-ms.hanypay.co/apikeys/merchant/user/USER_ID_HERE/balance/ \
  -H "Hanypay-Api-Key: YOUR_API_KEY"
Response (200)
{
  "status": true,
  "message": "Balance retrieved.",
  "data": {
    "user_id": "a1b2c3d4-...",
    "wallets": [
      {
        "wallet_number": "1234567890123456",
        "currency_code": "AKL",
        "balance": "500.000000"
      }
    ]
  }
}

Rate limit: 5 requests per minute per API key. Error codes: invalid_user, no_consent

POST

Name Check

POST /name-check/

Confirm who a wallet belongs to before sending money to it. Wallet transfers cannot be reversed, so a single mistyped digit sends funds to a stranger — this is the check that catches it first.

FieldTypeRequiredDescription
wallet_numberstring(16)RequiredThe recipient's wallet number
account_namestringOptionalThe name you expect. Supply it and the response tells you whether it matches; omit it and you get the account name alone.
curl
curl -X POST https://payment-ms.hanypay.co/api/v1/merchant/name-check/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "wallet_number": "9123456789012345",
    "account_name": "John Mensah"
  }'
Response (200)
{
  "success": true,
  "message": "Account name resolved.",
  "data": {
    "wallet_number": "9123456789012345",
    "account_name": "John Mensah",
    "account_type": "user",
    "match": "match",
    "match_score": 1.0,
    "currency": "GHS"
  }
}
FieldTypeRequiredDescription
matchstringRequired"match" — proceed. "close_match" — show the payer the masked name and let them confirm. "no_match" — warn before sending. "unchecked" — you sent no name to compare.
match_scorenumberOptional0 to 1. Null when no name was supplied to check.
account_typestringRequired"user" or "business"
Treat name_unavailable (503) as “cannot confirm”, never as a mismatch. It means the lookup failed, not that the recipient is wrong — blocking the transfer on it would stop legitimate payments during a brief outage.

Rate limit: 30 requests per minute per API key. Error codes: wallet_not_found (404), name_unavailable (503)

Collections

POST

Initiate Payment

POST /payments/initiate/

Start a payment collection. The customer receives a 6-digit authorization code via email which must be verified to complete the payment.

FieldTypeRequiredDescription
wallet_numberstring(16)RequiredCustomer's wallet number
amountdecimalRequiredAmount in AKL (min 0.000001)
currency_codestring(3)OptionalCurrency code (only "AKL", default "AKL")
txn_referencestring(256)OptionalYour unique reference for idempotency
metadataobjectOptionalArbitrary key-value data returned in webhooks
callback_urlurlOptionalURL to receive webhook notifications
charge_bearerstringOptional"customer" (default) or "merchant". Determines who pays the transaction fee.
Set charge_bearer to "merchant" to absorb the transaction fee yourself. The customer will only be charged the payment amount. Default is "customer", where the fee is added on top of the amount.
curl
curl -X POST https://payment-ms.hanypay.co/api/v1/merchant/payments/initiate/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "wallet_number": "1234567890123456",
    "amount": "100.00",
    "txn_reference": "ORDER-001",
    "charge_bearer": "merchant"
  }'
Response (200)
{
  "success": true,
  "message": "Authorization code has been sent to customer.",
  "data": {
    "transaction_id": "d4f8a2b1-...",
    "txn_reference": "ORDER-001",
    "verification_required": true
  }
}
If you provide a txn_reference that already exists, the existing transaction is returned instead of creating a duplicate. Use this for safe retries.
POST

Verify Payment

POST /payments/verify/

Submit the customer's authorization code to complete the payment. The result is synchronous — you get success or failure immediately.

FieldTypeRequiredDescription
codestringRequired6-character authorization code from customer
txn_referencestringRequiredTransaction reference from initiate response
curl
curl -X POST https://payment-ms.hanypay.co/api/v1/merchant/payments/verify/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "code": "ABC123",
    "txn_reference": "ORDER-001"
  }'
Response (200)
{
  "success": true,
  "message": "Payment completed successfully.",
  "data": {
    "transaction_id": "d4f8a2b1-...",
    "status": "Completed",
    "amount": "100.000000"
  }
}

Error codes: invalid_code, code_expired, invalid_reference, processing_failed

Payout / Disbursement

POST

Payout

POST /payouts/

Send AKL from your business wallet to a recipient wallet.

FieldTypeRequiredDescription
wallet_numberstring(16)RequiredRecipient wallet number
amountdecimalRequiredAmount in AKL
descriptionstring(255)OptionalPayout description
curl
curl -X POST https://payment-ms.hanypay.co/api/v1/merchant/payouts/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "wallet_number": "5555666677778888",
    "amount": "50.00",
    "description": "Salary payout"
  }'
Response (200)
{
  "success": true,
  "message": "Payout completed successfully.",
  "data": {
    "transaction_id": "a1b2c3d4-...",
    "reference": "REF-001",
    "status": "Completed"
  }
}

Error codes: invalid_wallet, limit_exceeded, same_wallet, payout_failed

Transactions

GET

List Transactions

GET /transactions/

Retrieve a paginated list of your business transactions.

FieldTypeRequiredDescription
statusstringOptionalFilter by status (Pending, Completed, Failed, etc.)
transaction_typestringOptionalFilter by type (Purchase, Transfer, Refund)
start_datedateOptionalFilter from date (YYYY-MM-DD)
end_datedateOptionalFilter to date (YYYY-MM-DD)
querystringOptionalSearch by reference or wallet number
pageintegerOptionalPage number (default 1)
page_sizeintegerOptionalResults per page (default 15)
curl
curl "https://payment-ms.hanypay.co/api/v1/merchant/transactions/?status=Completed&page=1" \
  -H "Hanypay-Api-Key: YOUR_API_KEY"
Response (200)
{
  "count": 50,
  "next": "...?page=2",
  "previous": null,
  "total_pages": 4,
  "current_page": 1,
  "results": [
    {
      "reference": "abc123",
      "txn_reference": "ORDER-001",
      "amount": "100.000000",
      "status": "Completed",
      ...
    }
  ]
}
GET

Get Transaction

GET /transactions/{reference}/

Retrieve a single transaction by its internal reference or your txn_reference.

curl
curl https://payment-ms.hanypay.co/api/v1/merchant/transactions/ORDER-001/ \
  -H "Hanypay-Api-Key: YOUR_API_KEY"
Response (200)
{
  "success": true,
  "message": "Transaction retrieved.",
  "data": {
    "id": 123,
    "reference": "abc123",
    "txn_reference": "ORDER-001",
    "amount": "100.000000",
    "charges": "1.500000",
    "currency_code": "AKL",
    "transaction_type": "Purchase",
    "status": "Completed",
    "channel": "Wallet Transfer",
    "description": "",
    "metadata": {},
    "created_at": "2026-01-15T10:30:00Z"
  }
}
POST

Refund

POST /refunds/

Refund a completed purchase transaction. Supports full and partial refunds. Cumulative refunds are tracked — the total cannot exceed the original amount.

FieldTypeRequiredDescription
transaction_referencestringRequiredReference of the original transaction
amountdecimalOptionalPartial refund amount (defaults to full amount)
descriptionstring(255)OptionalReason for refund
curl
curl -X POST https://payment-ms.hanypay.co/api/v1/merchant/refunds/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "transaction_reference": "ORDER-001",
    "amount": "25.00",
    "description": "Customer requested partial refund"
  }'
Response (200)
{
  "success": true,
  "message": "Refund completed successfully.",
  "data": {
    "refund_reference": "REF-REFUND-001",
    "amount": "25.0",
    "status": "Completed"
  }
}

Utilities

POST

Calculate Charges

POST /charges/

Preview the transaction fee for a given amount before initiating a payment.

FieldTypeRequiredDescription
amountdecimalRequiredTransaction amount
curl
curl -X POST https://payment-ms.hanypay.co/api/v1/merchant/charges/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{"amount": "100.00"}'
Response (200)
{
  "success": true,
  "message": "Charges calculated.",
  "data": {
    "amount": "100.000000",
    "charges": "1.500000",
    "total": "101.500000"
  }
}
POST

Exchange Rates

POST /exchange-rates/

Convert between AKL and other currencies. One of the currencies must be AKL.

FieldTypeRequiredDescription
base_currency_codestring(3)RequiredSource currency (e.g. "AKL")
target_currency_codestring(3)RequiredTarget currency (e.g. "USD")
amountdecimalRequiredAmount to convert
curl
curl -X POST https://payment-ms.hanypay.co/api/v1/merchant/exchange-rates/ \
  -H "Content-Type: application/json" \
  -H "Hanypay-Api-Key: YOUR_API_KEY" \
  -d '{
    "base_currency_code": "AKL",
    "target_currency_code": "USD",
    "amount": "100.00"
  }'
Response (200)
{
  "success": true,
  "message": "Exchange rate retrieved.",
  "data": {
    "base_currency": "AKL",
    "target_currency": "USD",
    "rate": 0.052,
    "original_amount": 100.0,
    "converted_amount": 5.2
  }
}

Guides

Payment Flow

The collection flow involves two API calls with a customer action in between:

1
Initiate Payment
Call POST /payments/initiate/ with the customer wallet number and amount.
2
Customer receives code
A 6-digit authorization code is sent to the customer via email.
3
Customer shares code
The customer provides the code to your application (e.g. via a form).
4
Verify Payment
Call POST /payments/verify/ with the code and txn_reference.
5
Payment completes
Success or failure is returned synchronously. A webhook fires to your registered URLs.

Webhooks

Webhook URLs are registered in the Dashboard under Business → API Keys → Webhook URLs. When a payment completes or fails, we send a POST request to each active URL.

Payload format

{
  "event": "payment.completed",
  "data": {
    "reference": "abc123",
    "txn_reference": "ORDER-001",
    "amount": "100.000000",
    "charges": "1.500000",
    "currency_code": "AKL",
    "status": "Completed",
    "transaction_type": "Purchase",
    "created_at": "2026-01-15T10:30:00Z"
  }
}

Events

EventDescription
payment.completedCustomer payment succeeded
payment.failedCustomer payment failed
payment.updatedPayment status changed
payout.completedDisbursement succeeded
payout.failedDisbursement failed

Signature verification

Each webhook includes an X-Hanypay-Signature header containing an HMAC-SHA256 hex digest of the JSON body signed with your webhook secret.

Python verification example
import hmac, hashlib

def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Error Reference

CodeDescription
invalid_api_keyMissing or invalid API key
invalid_walletWallet number not found
insufficient_balanceNot enough AKL balance
invalid_codeAuthorization code not found
code_expiredAuthorization code has expired
invalid_referenceTransaction reference not found
invalid_amountAmount exceeds allowed limit
processing_failedPayment processing failed
limit_exceededTransaction limit exceeded
same_walletSource and destination wallets are the same
payout_failedPayout processing failed
refund_failedRefund processing failed
conversion_failedCurrency conversion failed
business_not_approvedBusiness is pending admin approval
invalid_userUser not found or does not belong to your business
phone_takenPhone number already in use
creation_failedUser creation failed
no_consentUser has not granted wallet access
code_expiredOTP code has expired

Transaction Statuses

StatusDescription
PendingTransaction created, awaiting verification
InitiatedTransaction initiated with provider
CompletedTransaction completed successfully
FailedTransaction failed
CancelledTransaction cancelled
FundsReservedFunds reserved for transaction
API endpoints are rate-limited to 120 requests per minute per API key. Exceeding this limit returns HTTP 429. Implement exponential backoff in your integration.
Light Logo
Hanypay is a leading provider of online payment processing, and merchant solutions. Our mission is to make electronic payments seemless and accessible for businesses of all sizes.
Follow us
Copyright © 2026. Hanypay - All Rights Reserved.