Haminass Pay
Login

API Docs

Everything a developer needs to connect their platform to Haminass Pay

What this is, in one sentence

Your service asks Haminass Pay to create a payment, we handle the gateway directly (you never see NBC's or Vodacom's credentials), and we hand you back what you need to get the customer paying — a link for card, a phone prompt for M-Pesa. Pick a payment method on the left to see exactly how.

Your base URL

Every example on this page uses this — copy it as-is:

https://www.accesspay.eopsprimax.com

Getting an API key

Ask a Haminass Pay admin to create a platform for you on the Platforms page. They'll need to know your platform's name and which domain(s) you want customers redirected back to after paying (card only — M-Pesa doesn't need this). You'll get a key that looks like a long random string — treat it like a password: store it as a secret/environment variable on your server, never in code, chat, or a public repo.

The one required header

Every request to any endpoint on this page needs this, regardless of payment method:

Authorization: Bearer YOUR_API_KEY

Missing or wrong key → 401, with this body:

{"error": "Invalid API key"}
Create a payment — Card (NBC)
POST /api/v1/payments

Creates a new card payment and returns a link to NBC's hosted card-entry page. Your customer enters their card details there, never on your site or ours.

Request headers

HeaderRequired?Value
AuthorizationYesBearer YOUR_API_KEY
Content-TypeYesapplication/json
Idempotency-KeyRecommendedAny unique string you generate per attempt

Request body fields

FieldTypeRequired?Meaning
amountnumberYesIn normal currency units, not cents — 5000 means 5,000 TZS, not 50 TZS. We convert to the smallest unit internally.
currencystringYes3-letter ISO 4217 code, e.g. TZS, USD
payment_methodstringNo"card" — this is the default, so you can omit it entirely
emailstringNoCustomer's email, if you have it — stored with the order, not required by NBC
redirect_urlstringNoWhere NBC sends the customer's browser after they pay. Falls back to a default if omitted.

Full example

curl -X POST https://www.accesspay.eopsprimax.com/api/v1/payments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-2026-07-13-00042" \
  -d '{
    "amount": 5000,
    "currency": "TZS",
    "email": "customer@example.com",
    "redirect_url": "https://your-domain.com/payment-result/"
  }'
redirect_url's domain must be pre-approved. When your API key was created, an admin set a list of domains you're allowed to redirect to. Send a redirect_url on a different domain and you get 403 instead of a payment — this stops a leaked key from being used to redirect customers to a scam site.
Idempotency-Key protects against double charges. If your request times out and you don't know whether it succeeded, retry it with the same key — you get back the original payment, not a second one. Use something naturally unique to that attempt, like your own order ID.

Response — 200 OK

FieldTypeMeaning
orderReferencestringUnique ID for this payment — save it, you'll need it to check status later
paymentPageUrlstringSend your customer's browser here to pay
statusstringAlways "PENDING" at this point — nobody's paid yet
{
  "orderReference": "82732295-74f3-421d-a528-e0bacc474ddd",
  "paymentPageUrl": "https://paypage.nbc.ngenius-payments.com/?code=55ca6510320c9525",
  "status": "PENDING"
}

Error responses

StatusBodyWhat it means
401{"error": "Missing or invalid Authorization header"}No Authorization header sent at all
401{"error": "Invalid API key"}Key sent, but it's wrong or disabled
400{"error": "amount must be a valid number"}amount missing, or not a number
400{"error": "amount must be greater than zero"}amount was zero or negative
400{"error": "amount is too large"}amount exceeds what we can store — almost certainly a units mistake on your end
400{"error": "currency must be a 3-letter currency code"}currency missing, or not exactly 3 letters
403{"error": "redirect_url host '...' is not allowed for this client"}Your redirect_url isn't on your approved list
429{"error": "Too many requests. Please try again shortly."}More than 120 payment-creation requests in a minute — see the FAQ
502{"error": "Failed to create payment order"}NBC's own system didn't respond — not your fault, safe to retry (use the same Idempotency-Key)
Refund a card payment
POST /api/v1/payments/<orderReference>/refund

Refunds all or part of one of your own paid orders, back to the card the customer paid with. Can be called more than once on the same order for multiple partial refunds, up to the original amount.

Not currently available. This endpoint will return an error if called right now. Check back later, or ask whoever manages Haminass Pay for the current status.

Request body fields (all optional)

FieldTypeMeaning
amountnumberHow much to refund, in normal currency units. Omit it entirely to refund the full remaining amount.
reasonstringFree text, kept in our audit log — not shown to the customer

Full example — refund everything left

curl -X POST https://www.accesspay.eopsprimax.com/api/v1/payments/82732295-74f3-421d-a528-e0bacc474ddd/refund \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "customer requested cancellation"}'

Full example — partial refund

curl -X POST https://www.accesspay.eopsprimax.com/api/v1/payments/82732295-74f3-421d-a528-e0bacc474ddd/refund \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 1500, "reason": "one item out of stock"}'

Response — 200 OK

FieldTypeMeaning
orderReferencestringSame ID you called with
statusstringPARTIALLY_REFUNDED or REFUNDED
refundedAmountstringTotal refunded on this order so far, including this call and any earlier ones
totalAmountstringThe order's original amount, for comparison
{
  "orderReference": "82732295-74f3-421d-a528-e0bacc474ddd",
  "status": "REFUNDED",
  "refundedAmount": "5000.00",
  "totalAmount": "5000.00"
}

Error responses

StatusBodyWhat it means
404{"error": "Order not found"}Doesn't exist, or belongs to a different platform
400{"error": "Cannot refund 6000 TZS -- only 5000.00 TZS is left to refund."}Amount exceeds what's left to refund on this order
400{"error": "This order has already been fully refunded."}Nothing left to refund
400{"error": "NBC isn't currently offering a refund on this order..."}The order isn't in a refundable state right now
502{"error": "Could not process the refund with NBC"}See the callout above — check the order's status before retrying
A refund is real money movement and cannot be undone through this API — there's no "un-refund" endpoint. Double-check the amount before calling this in production.
Create a payment — M-Pesa
POST /api/v1/payments

Creates a new M-Pesa payment — sends an approval prompt directly to the customer's phone. Same endpoint as card, different payment_method.

Not currently available. This will return an error if called right now. Check back later, or ask whoever manages Haminass Pay for the current status. The request/response shape documented here is what to expect once it is available.

Request headers

HeaderRequired?Value
AuthorizationYesBearer YOUR_API_KEY
Content-TypeYesapplication/json
Idempotency-KeyRecommendedAny unique string you generate per attempt

Request body fields

FieldTypeRequired?Meaning
amountnumberYesIn normal currency units, not cents.
currencystringYesMust be TZS.
payment_methodstringYesMust be "mpesa"
phone_numberstringYesTanzanian MSISDN, e.g. 0712345678 or 255712345678. The customer gets the approval prompt on this phone.

Full example

curl -X POST https://www.accesspay.eopsprimax.com/api/v1/payments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-2026-07-13-00043" \
  -d '{
    "amount": 5000,
    "currency": "TZS",
    "payment_method": "mpesa",
    "phone_number": "0712345678"
  }'
Idempotency-Key protects against double charges. If your request times out and you don't know whether it succeeded, retry it with the same key — you get back the original payment, not a second one.

Response — 200 OK (once available)

No paymentPageUrl — there's no hosted page, the prompt goes straight to the customer's phone. A message field replaces it.

FieldTypeMeaning
orderReferencestringUnique ID for this payment — save it, you'll need it to check status later
statusstringAlways "PENDING" at this point
messagestringHuman-readable, safe to show your customer as-is
{
  "orderReference": "a1b2c3d4e5f6...",
  "status": "PENDING",
  "message": "Check your phone to approve the M-Pesa payment request."
}

Error responses

StatusBodyWhat it means
401{"error": "Missing or invalid Authorization header"}No Authorization header sent at all
401{"error": "Invalid API key"}Key sent, but it's wrong or disabled
400{"error": "amount must be a valid number"}amount missing, or not a number
400{"error": "amount must be greater than zero"}amount was zero or negative
400{"error": "M-Pesa payments must be in TZS"}currency wasn't TZS
400{"error": "phone_number must be a valid Tanzanian number, e.g. 0712345678 or 255712345678"}Missing or invalid phone number
429{"error": "Too many requests. Please try again shortly."}More than 120 payment-creation requests in a minute — see the FAQ
502{"error": "Failed to initiate M-Pesa payment"}See the callout above
M-Pesa doesn't have a refund endpoint yet — see the Card tab for how refunds work once they're available; that mechanism is card-only for now.

These two endpoints work the same way regardless of payment method — a M-Pesa order and a card order both show up here with a paymentMethod field telling you which is which.

Check one payment's status
GET /api/v1/payments/<orderReference>

Looks up a single payment you already created, by the reference you got back when creating it.

Full example

curl https://www.accesspay.eopsprimax.com/api/v1/payments/82732295-74f3-421d-a528-e0bacc474ddd \
  -H "Authorization: Bearer YOUR_API_KEY"

Response — 200 OK

FieldTypeMeaning
orderReferencestringSame ID you looked up
statusstringOne of PENDING, PAID, FAILED, CANCELLED, EXPIRED (auto-closed after 48h with no confirmed outcome), PARTIALLY_REFUNDED, REFUNDED
paymentMethodstring"CARD" or "MPESA"
amountstringDecimal string, e.g. "5000.00" — always 2 decimal places
currencystringThe currency you created it with
phoneNumberstring or nullM-Pesa orders only
createdAtstringISO 8601 timestamp, UTC
updatedAtstringSame format — changes when the status changes
{
  "orderReference": "82732295-74f3-421d-a528-e0bacc474ddd",
  "status": "PAID",
  "paymentMethod": "CARD",
  "amount": "5000.00",
  "currency": "TZS",
  "phoneNumber": null,
  "createdAt": "2026-07-13T08:30:47.551072+00:00",
  "updatedAt": "2026-07-13T08:31:02.112904+00:00"
}

Error responses

StatusBodyWhat it means
401{"error": "Invalid API key"}Bad or missing key
404{"error": "Order not found"}Either that reference doesn't exist, or it belongs to a different platform — you'll never be told which, both look identical from the outside
We also notify you automatically when a payment's status changes (a server-to-server webhook/callback from the gateway updates it on our side the moment it happens) — this endpoint is a convenient way to double-check, not the only way you'll find out.
List all of your payments
GET /api/v1/payments

Returns every payment your platform has ever created here — card and M-Pesa mixed together, newest first, paginated. This is how you'd build your own "payment history" screen.

Query parameters (all optional, add as ?param=value)

ParamTypeDefaultMeaning
pagenumber1Which page of results
page_sizenumber25Results per page, capped at 100
statusstring(none)Only show one status: PENDING, PAID, FAILED, CANCELLED, EXPIRED, PARTIALLY_REFUNDED, or REFUNDED

Full example

curl "https://www.accesspay.eopsprimax.com/api/v1/payments?page=1&page_size=25&status=PAID" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response — 200 OK

FieldTypeMeaning
countnumberTotal matching payments, across all pages
pagenumberWhich page this response is
totalPagesnumberHow many pages exist in total
hasNextbooleanWhether page + 1 exists
hasPreviousbooleanWhether page - 1 exists
resultsarrayThe payments themselves — same fields as the single-status endpoint, plus email
{
  "count": 42,
  "page": 1,
  "totalPages": 2,
  "hasNext": true,
  "hasPrevious": false,
  "results": [
    {
      "orderReference": "82732295-74f3-421d-a528-e0bacc474ddd",
      "status": "PAID",
      "paymentMethod": "CARD",
      "amount": "5000.00",
      "currency": "TZS",
      "email": "customer@example.com",
      "phoneNumber": null,
      "createdAt": "2026-07-13T08:30:47.551072+00:00",
      "updatedAt": "2026-07-13T08:31:02.112904+00:00"
    },
    {
      "orderReference": "37930c8d-b070-4bbf-9cd8-16891c67fb3d",
      "status": "PENDING",
      "paymentMethod": "MPESA",
      "amount": "1000.00",
      "currency": "TZS",
      "email": null,
      "phoneNumber": "255712345678",
      "createdAt": "2026-07-13T08:46:33.408918+00:00",
      "updatedAt": "2026-07-13T08:46:33.408918+00:00"
    }
  ]
}
Only ever returns your own platform's payments — there's no parameter or trick that shows another platform's data. If you've only just started, an empty results: [] with count: 0 just means you haven't created any yet.
Send a payout
POST /api/v1/payouts

Sends money out to someone directly — not tied to any prior payment. Different from a refund: there's no existing order involved.

Not currently available. This endpoint will return an error if called right now. Check back later, or ask whoever manages Haminass Pay for the current status.

Request body fields

FieldTypeRequired?Meaning
recipient_namestringYesWho's receiving the money
recipient_identifierstringYesPhone number, bank account number, or IBAN — exact requirements not yet confirmed
amountnumberYesNormal currency units, not cents
currencystringYes3-letter ISO 4217 code
reasonstringNoFree text, kept in our audit log

Full example

curl -X POST https://www.accesspay.eopsprimax.com/api/v1/payouts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient_name": "Jane Customer",
    "recipient_identifier": "255700000000",
    "amount": 5000,
    "currency": "TZS",
    "reason": "goodwill refund via mobile money"
  }'

Response — 200 OK (once enabled)

{
  "reference": "payout-1a2b3c4d5e6f7a8b9c0d1e2f",
  "status": "SENT"
}

Error responses

StatusBodyWhat it means
400{"error": "recipient_name and recipient_identifier are required"}Missing a required field
502{"reference": "...", "status": "FAILED", "error": "NBC rejected the payout"}See the callout above
Check a payout's status / list your payouts
GET /api/v1/payouts/<reference>
GET /api/v1/payouts

Same pattern as payments: look up one payout by reference, or list all of your platform's payouts (paginated, supports page/page_size, newest first).

Full example — one payout

curl https://www.accesspay.eopsprimax.com/api/v1/payouts/payout-1a2b3c4d5e6f7a8b9c0d1e2f \
  -H "Authorization: Bearer YOUR_API_KEY"

Response — 200 OK

{
  "reference": "payout-1a2b3c4d5e6f7a8b9c0d1e2f",
  "status": "FAILED",
  "recipientName": "Jane Customer",
  "recipientIdentifier": "255700000000",
  "amount": "5000.00",
  "currency": "TZS",
  "reason": "goodwill refund via mobile money",
  "createdAt": "2026-07-14T09:00:00.000000+00:00",
  "updatedAt": "2026-07-14T09:00:01.000000+00:00"
}

Full example — list

curl "https://www.accesspay.eopsprimax.com/api/v1/payouts?page=1&page_size=25" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response shape matches the payments list endpoint (count, page, totalPages, hasNext, hasPrevious, results) — results contains objects with the same fields as the single-payout response above.

Building your own transactions table

A common thing to build on your side: a table showing your platform's own payment history, with columns like Reference, Amount, Method, Status, Email/Phone, Created, plus whatever action buttons make sense for you. Note there's no "Platform" column on your side — every row already belongs to you, that column only makes sense on our internal admin view where staff see multiple platforms at once.

Example — JavaScript (fetch)

async function loadMyPayments(page = 1) {
  const res = await fetch(
    `https://www.accesspay.eopsprimax.com/api/v1/payments?page=${page}&page_size=25`,
    { headers: { Authorization: "Bearer YOUR_API_KEY" } }
  );
  const data = await res.json();

  // data.results is your row data -- render it into your own table:
  // Reference      -> row.orderReference
  // Amount         -> `${row.amount} ${row.currency}`
  // Method         -> row.paymentMethod
  // Status         -> row.status
  // Email/Phone    -> row.email ?? row.phoneNumber ?? "—"
  // Created        -> new Date(row.createdAt).toLocaleString()
  // Actions        -> whatever your own UI needs
  return data;
}

Example — Python (requests)

import requests

def load_my_payments(page=1, page_size=25, status=None):
    params = {"page": page, "page_size": page_size}
    if status:
        params["status"] = status

    response = requests.get(
        "https://www.accesspay.eopsprimax.com/api/v1/payments",
        headers={"Authorization": "Bearer YOUR_API_KEY"},
        params=params,
    )
    response.raise_for_status()
    return response.json()  # ["results"] is your row data, same fields as above
Frequently asked questions

Is amount in cents or whole units?

Whole units. Send 5000 for 5,000 TZS, not 500000. We convert to the smallest currency unit internally before talking to the gateway — you never need to think about that conversion.

What timezone are the timestamps in?

UTC, always, in ISO 8601 format (2026-07-13T08:30:47.551072+00:00). Convert to your own local timezone on your side for display.

Is there a sandbox / test mode?

No — this integration talks to the live gateways. There is no separate sandbox environment. Use small real amounts when testing, and note test transactions still show up in your payment history like any other.

How is M-Pesa different from card, from my side?

You send phone_number instead of redirect_url/email, and there's no paymentPageUrl to redirect your customer to — the approval prompt appears directly on their phone. Everything else is identical: same endpoint, same auth, same idempotency handling, same status-check and list endpoints, same webhook/callback notification once they approve or decline.

Is there a rate limit?

Yes, per platform (your key, not shared with anyone else): creating a payment is limited to 120 requests/minute, refunds and payouts to 30/minute each, and status polling (any GET endpoint) to 300/minute. Going over any of these returns 429 with {"error": "Too many requests. Please try again shortly."}. In practice you shouldn't be near any of these — the webhook already tells you the moment a payment's status changes, so polling should be a backup, not your primary mechanism.

My redirect_url keeps getting rejected with 403 — why?

Its domain isn't on your platform's approved list. Ask whoever manages your Haminass Pay platform entry (on the Platforms page) to add it, or drop redirect_url entirely to use the default. Card only — M-Pesa doesn't use this.

I created a payment but never got a webhook — what do I do?

Poll GET /api/v1/payments/<orderReference> for that specific order to check its real status directly, rather than waiting indefinitely.

Can I cancel or refund a payment through this API?

Refund — see the Card tab (card payments only), but as flagged there, it's not currently available. There's no separate "cancel" endpoint; a PENDING order that's never paid just stays PENDING and eventually auto-expires (see the EXPIRED status) rather than needing to be explicitly cancelled.

Can I send money to a customer who didn't pay me, through this API?

That's the Payouts tab — but as flagged there, it's not currently available. The endpoint exists and is documented so you're ready for it once it is.

What happens if I lose my API key?

Ask an admin to rotate it for you on your platform's page (Platforms → your platform → Rotate key). The old key stops working immediately, so coordinate the swap on your side first.