Billing and Safe Retries
Understand the signing charge, definitive reversals, ambiguous outcomes, idempotency, and production retry rules.
Invoice signing is the only billable Gateway operation. Your top-level business in Duplo Dashboard pays the current NGN signing fee for every represented business on its approved domain. The caller cannot choose the payer wallet, fee amount, fee destination, state, or mode.
Validation, NRS resource reads, entity and recipient lookups, health checks, transmission, represented-business onboarding, and Stored invoice reads do not create a Gateway signing charge.
The precise charging promise
Only invoice signing is billable. A definitive NRS rejection starts an automatic compensating reversal. If NRS may have committed the sign but Duplo cannot prove the outcome, the charge is retained temporarily for authoritative reconciliation. This avoids reversing a real sign and then charging again for a duplicate.
Pass-through billing sequence
Bind
Gateway validates host, state, represented business, IRN, business ID, supplier TIN, and the idempotency key and attempt number identifying this attempt.
Debit
Duplo resolves the current fee and payer wallet, then records one idempotent billing attempt.
Protect
Gateway persists recovery context before it is allowed to contact NRS.
Classify
A valid sign succeeds; a definite rejection reverses; an uncertain outcome enters reconciliation.
| Outcome | Did Gateway call NRS? | Billing action | Client response |
|---|---|---|---|
| Wallet debit declined | No | No successful charge | 402 or 422 with Wallet debit was declined |
| Recovery state cannot be persisted after debit | No | Safe reversal is started | 503 with a traceId |
| NRS returns valid sign success | Yes | Charge remains | NRS success body, requiring HTTP 2xx, body code: 201, and data.ok: true |
| NRS returns an eligible, structurally valid 4xx rejection | Yes | Reversal is started | Sanitized 502 Invoice signing failed with traceId |
| Timeout, transport failure, NRS 5xx, malformed body, unexpected 2xx, or NRS 408/409/425/429 | Maybe | Charge is retained for reconciliation | 502 unconfirmed outcome with traceId |
The public error is intentionally bounded so dependency details and internal billing identifiers do not leak.
{
"message": "Invoice signing failed",
"traceId": "req-018f0f56-f5f5-7c98-a21a-4a03c11a85b1"
}{
"message": "Invoice signing outcome is unconfirmed and requires reconciliation",
"traceId": "req-018f0f56-f5f5-7c98-a21a-4a03c11a85b1"
}Stored-mode billing
202 Accepted in Stored mode is not a billing event. It means the command and invoice record were accepted for asynchronous processing.
The processor validates the invoice first, then creates the signing debit immediately before NRS signing. It records the resulting NRS and billing state on the Stored invoice. A validation failure before sign is not billed. Poll GET /invoices/{invoiceId} until the record is terminal; do not infer sign success from the intake response or elapsed time.
Inspect firsRejectionReason whenever Stored processing ends at created / null after validation or rejected / rejected after signing/billing work. Keep the invoice ID and request trace. Do not create a replacement until you understand whether the original IRN may already exist; the current Gateway does not expose an edit/resubmit command for that record.
Design stable idempotency keys
Use one key for one logical command. Keys on Gateway management commands must be 8 to 128 characters long and use only letters, digits, dots, underscores, colons, and hyphens.
onboard:<customer-id>:v1
qr-material:<gateway-sub-business-id>:2026-08
stored-sign:<erp-invoice-id>:v1
stored-transmit:<duplo-invoice-id>:v1
pass-sign:<erp-invoice-id>:attempt-1Keep the key in the same durable row as your invoice or onboarding command. Never generate it inside a network retry loop.
| Command | Required? | Replay behavior |
|---|---|---|
POST /sub-businesses | Yes | nrsCredentials is required while the intent is new or pending. A replay of a completed onboarding that sends the same optional profile fields may omit both nrsCredentials and nrsCryptoKeys, and returns the original identifiers. Changing any profile value under the same key returns 409. |
POST /sub-businesses/{id}/nrs-crypto-material | Yes | An identical store-or-replace request is deduplicated; changed QR material needs a new key |
Pass-through POST /api/v1/invoice/sign | Key recommended; attempt number defaults to 1 | Keep x-idempotency-key and positive x-attempt-number together; a successful billing replay is blocked with 409 because the original NRS body is not retained |
Stored POST /api/v1/invoice/sign | Yes | Exact logical document reuses the accepted command; changed content under the key conflicts |
POST /invoices/{id}/transmit | Yes | An identical Stored transmission command is deduplicated; a changed represented-business or invoice binding conflicts |
For Stored intake, idempotency also binds the approved domain configuration, its canonicalized domain label, the current test/live state, the mode version, the represented business, and the recursively canonicalized invoice body. Reordering JSON object keys does not change the fingerprint; changing a value or reordering an array does.
For Pass-through signing, Duplo also identifies the billing attempt by business, domain, IRN, and x-attempt-number. Re-sending the same logical attempt keeps both headers unchanged. After a conclusively failed or reversed attempt, a separately authorized attempt must use the next positive attempt number and a new stable idempotency key. Changing only the key can still return 409. Never advance either value for an unconfirmed outcome.
Retry decision table
| Operation and result | Retry automatically? | Safe action |
|---|---|---|
| Represented-business onboarding timed out before completion | Yes, bounded | Resend the same optional profile fields with the same idempotency key, and include nrsCredentials while the intent may still be pending |
| Completed onboarding response was lost | Yes | Replay the same optional profile fields with the same idempotency key. nrsCredentials and nrsCryptoKeys may both be omitted once the intent has completed |
NRS GET read failed before a response | Usually, with bounded backoff | Retry the same URL; respect rate limits and your total time budget |
| Unbilled validation returned a definite document error | No | Correct the invoice, assign a deliberate command/version, then validate again |
| Unbilled validation had a transport timeout | Yes, bounded | Re-send the same document; validation does not create a signing debit |
| Pass-through sign returned normal NRS success | No | Store the response and move to lookup/transmission |
Pass-through sign returned 402 or 422 | No immediate retry | Restore balance or account readiness; once failure is conclusive, use a new key and incremented attempt number |
Pass-through sign returned 409 replay | No | Recover the original response or verify the IRN; do not create random keys to bypass the guard |
Pass-through sign returned definitive 502 | No blind retry | Keep the trace, confirm reversal, correct the cause, then use a new key and incremented attempt number |
Pass-through sign returned unconfirmed 502 | No | Keep the original document and traceId; check the IRN authoritatively and contact Duplo support |
Stored intake timed out before 202 was received | Yes | Re-send the exact command with the same idempotency key |
Stored intake returned 202 | No new intake | Poll the returned invoice ID |
| Stored status is still pending | Yes, read only | Poll with bounded exponential backoff and jitter |
| Transmission failed or timed out | Do not re-sign | Read current invoice/IRN state, then retry only the original transmission operation if safe |
A practical backoff policy
For read-only polling, start around one second, double up to a reasonable ceiling such as 30 seconds, add jitter, and enforce a total workflow deadline. Retry-After, when supplied, takes precedence. Stop on a terminal invoice state or a customer-visible 4xx that requires input.
Do not apply this generic policy to a Pass-through sign. A transport error after a debit is not evidence that NRS did nothing.
Status guide
| HTTP status | Typical Gateway meaning | First action |
|---|---|---|
400 | Malformed JSON, header, or UUID; an invalid idempotency key; a server-owned field; or a mismatch between the represented business and the body identity | Correct the request; do not retry unchanged |
401 | Missing, invalid, expired, or revoked API key; invalid NRS portal login during onboarding | Re-authenticate or rotate the right credential |
402 / 422 | Signing debit declined, or onboarding NRS identity is inactive/incomplete | Check wallet/account or NRS readiness |
403 | The API key does not own the represented business, an IRN is not bound to it, or Duplo is not that business's Access Point Provider in NRS | Stop; repair the tenant mapping or the NRS link |
404 | Domain is unavailable, the represented business or invoice is hidden or absent, or a supplied NRS TIN did not match | Verify host, state, stored IDs, and NRS registration |
409 | State/mode drift, idempotency conflict, ambiguous active NRS-business selection, Pass-through replay, or operation incompatible with approved mode | Read current configuration before another command |
413 | Buffered request exceeds the Gateway limit | Reduce payload size; do not split one NRS invoice arbitrarily |
429 | Rate limit reached | Honor Retry-After when supplied; otherwise use bounded jittered backoff |
502 | NRS/dependency failure or a sanitized sign rejection/ambiguity | Read the message; preserve traceId; never blindly retry sign |
503 | Registry, authorization, billing, or recovery service unavailable | Retry safe reads later; treat sign state cautiously |
An ordinary proxied NRS route can return an NRS status and body that is not in this table. Protected and management routes can return a Gateway error instead. See NRS API response ownership.
Reconcile an unconfirmed sign
When a Pass-through sign returns the unconfirmed 502:
- Freeze automatic sign retries for that IRN and represented business.
- Persist the exact request bytes, IRN,
gatewaySubBusinessId, idempotency key, resolved attempt number (1when omitted), request time, and returnedtraceId. - Check your own response/archive first. If the original success was stored elsewhere, use it.
- Use an authoritative NRS lookup appropriate to the signed IRN and preserve that response. NRS owns the lookup schema, so do not treat a missing optional field as proof that signing failed.
- Contact Duplo with the support packet below. Wait for the billing/NRS outcome before starting a new sign attempt.
Do not send internal billing identifiers; the public traceId is sufficient for Duplo to correlate the attempt.
Support packet
Provide:
- Gateway hostname and test/live state, but never the complete API key.
- UTC timestamp and your
x-request-id. - Returned
traceId, HTTP status, and sanitized response body. - IRN and
gatewaySubBusinessId. - Idempotency key and attempt number.
- Mode and mode version shown in your approved domain configuration.
- Whether you observed a wallet movement and any authoritative NRS lookup result.
Remove passwords, private keys, Duplo Dashboard secret keys, full API keys, customer personal data, and raw invoice line data unless Duplo asks for it through an approved secure channel.
How is this guide?
Last updated on