Developers
Bank payment API
For banks: push every credit into a National Employment Council account to NECMAS, and it is recorded on the employer's levy account automatically.
Overview
One API serves every council on NECMAS. The bank sends each credit to a council account as it happens. NECMAS:
- stores the message exactly as received and answers straight away with a NECMAS receipt number;
- finds the council from the account credited;
- finds the employer from the payment reference (the employer code, e.g.
NECCHI120); - records the payment on the employer's levy account and emails the employer a receipt.
Payments that cannot be matched are kept and allocated by council staff. Nothing the bank sends is dropped.
| Base URL | https://necmas.com/api/banks/{bank}/v1 |
|---|---|
{bank} | Your bank code, given to you by NECMAS (e.g. zb) |
| Format | JSON over HTTPS (TLS 1.2+), UTF-8, Content-Type: application/json |
Getting access
NECMAS issues each bank two separate sets of credentials:
- Test, for building and checking your integration. Test payments are stored and matched but never reach a council's books.
- Live, issued once testing is complete.
Each set has an API key (sent with every request) and a signing secret (never sent; used to sign). Send us the public IP addresses your requests come from: keys can be limited to them. Lost or exposed secrets are revoked and reissued.
Signing requests
Every request carries three headers:
| Header | Value |
|---|---|
X-Api-Key | Your API key |
X-Timestamp | Current Unix time in seconds. Requests more than 5 minutes off are refused. |
X-Signature | Lower-case hex HMAC-SHA256 of timestamp + "." + raw request body, keyed with your signing secret. For GET requests the body is empty. |
Sign the exact bytes you send. Example:
BODY='{"transactions":[ ... ]}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST https://necmas.com/api/banks/zb/v1/payments \
-H "Content-Type: application/json" \
-H "X-Api-Key: $API_KEY" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \
--data "$BODY"
// Java
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(UTF_8), "HmacSHA256"));
String signature = HexFormat.of().formatHex(mac.doFinal((timestamp + "." + body).getBytes(UTF_8)));
Send payments
POST /payments with one transaction, or up to 500 in a transactions list.
{
"transactions": [
{
"reference": "FT26101400123",
"type": "credit",
"account_number": "4140123456789",
"amount": 2600,
"currency": "ZWG",
"value_date": "2026-10-14",
"payment_reference": "NECCHI120 levy Sept",
"narrative": "RTGS CREDIT SUNRISE LODGE",
"depositor_name": "SUNRISE LODGE (PVT) LTD",
"channel": "RTGS"
}
]
}
| Field | Description | |
|---|---|---|
reference | required | Your unique transaction reference (max 120). Sending the same reference again is safe: it is never recorded twice. |
type | optional | credit (default) or reversal |
account_number | required | The council account credited |
amount | required | Positive amount, 2 decimals |
currency | required | USD or ZWG (also accepted: ZiG, 840, 924) |
value_date | required | YYYY-MM-DD |
payment_reference | recommended | The reference the payer entered. It should hold the employer code. |
narrative | optional | Full narrative / description line (max 1000) |
depositor_name | recommended | Name of the paying account holder or depositor |
channel | optional | e.g. RTGS, CASH, INTERNAL, MOBILE |
Reply (HTTP 200). Each transaction has its own result:
{
"status": "ok",
"results": [
{ "reference": "FT26101400123", "receipt": "BP000123", "status": "accepted", "error": null }
]
}
| Result status | Meaning |
|---|---|
accepted | Stored. Processing continues on our side; no further action for the bank. |
duplicate | Already received earlier; the original receipt is returned. |
rejected | Not stored (see error). Fix and send again. |
Reversals
When a credit is reversed, send a transaction with "type": "reversal", its own new reference, and original_reference set to the credit's reference. The payment is removed from the employer's account.
{ "reference": "RV26101500077", "type": "reversal", "original_reference": "FT26101400123",
"account_number": "4140123456789", "amount": 2600.00, "currency": "ZWG", "value_date": "2026-10-15" }
Check a payment
GET /payments/{reference} (signed, empty body).
{ "status": "ok", "reference": "FT26101400123", "receipt": "BP000123",
"state": "applied", "received_at": "2026-10-14T09:12:44+00:00" }
state: accepted (held for council staff), applied (on the employer's account), reversed, closed, or test_complete for test payments.
Test your key
GET /ping (signed) answers {"status":"ok","environment":"test", ...} when the key, IP address and signature are right.
Responses and errors
Errors return {"status":"error","error":"code","message":"..."}.
| HTTP | error | Meaning |
|---|---|---|
| 400 | invalid_json | The body is not a JSON object |
| 401 | missing_auth | A signing header is missing |
| 401 | invalid_key | Unknown or revoked API key |
| 401 | stale_timestamp | X-Timestamp is more than 5 minutes from our clock |
| 401 | invalid_signature | The signature does not match the body |
| 403 | ip_not_allowed | The request came from an IP not allowed for this key |
| 404 | unknown_bank / not_found | Wrong bank code / no payment with that reference |
| 429 | Too many requests (more than 600 a minute); retry after a short wait | |
| 5xx | Our side; retry with the same reference |
If you get no answer or a 5xx, send the same transaction again: the reference keeps it from being recorded twice.
Test environment
Use the same URL with your test key. Test payments are matched exactly like live ones but never posted. The status check for a test payment adds test_detail (how it was matched) so you can confirm your integration end to end.
Questions or access: contact NECMAS.