neo-pays

Testing

A test key moves no real money — but almost everything else is real.

Test keys

Your keys come in two families: sk_live_… and sk_test_…. The key you use marks the operation for its whole life; nothing moves an operation from one family to the other.

With a test key:

  • no provider is ever called. A simulation answers instead, always — a test run cannot reach a real rail, or disturb one.
  • the ledger, settlements and transactional notifications are excluded. No accounting movement, no settlement total, no SMS or email to the payer.
  • everything else is real: the acceptance chain, your webhooks, your developer logs, your reporting rows, and the whole of the risk checks.

Magic amounts

The last two digits of the amount choose the outcome. It is the only way to rehearse the failure paths, which a healthy rail never produces.

Amount ends in What happens
99 Refused at submit, permanently. The "invalid number" class — do not retry
98 Refused at submit, temporarily. The "rail outage" class — retrying makes sense
97 Accepted, then failed. pending first, failed after
anything else Accepted, then success

This applies to collections, payouts, refunds and reversals alike.

The 97 case is the one actually worth rehearsing

It is the sequence integrations miss: your webhook receives pending, you mark the order "awaiting payment", and then failed arrives and must undo what pending did. Without a way to provoke it, the first time that path runs is in production.

An amount that encoded a failure fails all the way through: an operation cannot both refuse and succeed, however many times you read it.

Test identities

The compliance checks rehearse the same way, with magic identities. They match test operations only.

Identity What it rehearses What you should see
+221700000001 — "Test Sanctionne Fort" A strong match on a sanctions list The payment is refused.
"Test Sanctionne Nom Seul" (name only, no number) A name-only match Accepted, then held for review — see below
+221700000003 — "Test Personne Politiquement Exposee" A politically exposed person Enhanced review, not a refusal
+221700000004 — "Test Identite Mineure" An identity known to be a minor Refused on a restricted corridor (gaming, betting), accepted elsewhere

Identity set in force: fixture-2026-09-13-1. It is identical in every environment and changes by version — a test written against a named set stays reproducible.

A name alone is never a refusal

The second row surprises people, and it is deliberate: a match on the name alone does not stop a payment. Surnames are shared; refusing on a name would mean refusing a great many people for spelling it correctly. So the payment is accepted and held for review, and your integration should treat that as a wait, not a failure.

A refusal needs a strong identifier — a number, a document.

⚠️ These identities are not real people, and they never match a production payment. Using them with an sk_live_ key does nothing at all: they are rehearsal identities and that is all they are.

What you can rehearse

  • risk.status moving to review and to held, on the webhook and on a read;
  • the payment.held and payment.released events;
  • a compliance case opened in the test lane: visible to you, and it never reaches our compliance team or any regulatory total.

What a test run does not show you

Three limits, said plainly rather than discovered later:

  1. No accounting movement. The ledger is outside the test perimeter, so nothing about an available balance, a hold or a release of funds can be verified here.
  2. The timing is not production timing. The simulation has no payer: the confirmation comes from a sweep on its own cadence, not seconds after a USSD code. Test the transition, never the clock.
  3. Transactional notifications are not sent. Neither to the payer nor to you.

Do not

  • Do not derive your production timings from your test timings. See point 2.
  • Do not treat pending as a failure, in test or in production.
  • Never copy a test identity into your production data. It matches nothing there, and rehearsal data in a real record is a false trail for whoever reads it next.