Skip to content
Built in Zimbabwe for National Employment Councils
NECMAS Employment Council Systems
Menu

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:

  1. stores the message exactly as received and answers straight away with a NECMAS receipt number;
  2. finds the council from the account credited;
  3. finds the employer from the payment reference (the employer code, e.g. NECCHI120);
  4. 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 URLhttps://necmas.com/api/banks/{bank}/v1
{bank}Your bank code, given to you by NECMAS (e.g. zb)
FormatJSON 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:

HeaderValue
X-Api-KeyYour API key
X-TimestampCurrent Unix time in seconds. Requests more than 5 minutes off are refused.
X-SignatureLower-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"
        }
    ]
}
FieldDescription
referencerequiredYour unique transaction reference (max 120). Sending the same reference again is safe: it is never recorded twice.
typeoptionalcredit (default) or reversal
account_numberrequiredThe council account credited
amountrequiredPositive amount, 2 decimals
currencyrequiredUSD or ZWG (also accepted: ZiG, 840, 924)
value_daterequiredYYYY-MM-DD
payment_referencerecommendedThe reference the payer entered. It should hold the employer code.
narrativeoptionalFull narrative / description line (max 1000)
depositor_namerecommendedName of the paying account holder or depositor
channeloptionale.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 statusMeaning
acceptedStored. Processing continues on our side; no further action for the bank.
duplicateAlready received earlier; the original receipt is returned.
rejectedNot 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":"..."}.

HTTPerrorMeaning
400invalid_jsonThe body is not a JSON object
401missing_authA signing header is missing
401invalid_keyUnknown or revoked API key
401stale_timestampX-Timestamp is more than 5 minutes from our clock
401invalid_signatureThe signature does not match the body
403ip_not_allowedThe request came from an IP not allowed for this key
404unknown_bank / not_foundWrong bank code / no payment with that reference
429Too many requests (more than 600 a minute); retry after a short wait
5xxOur 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.