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

  1. Bind

    Gateway validates host, state, represented business, IRN, business ID, supplier TIN, and the idempotency key and attempt number identifying this attempt.

  2. Debit

    Duplo resolves the current fee and payer wallet, then records one idempotent billing attempt.

  3. Protect

    Gateway persists recovery context before it is allowed to contact NRS.

  4. Classify

    A valid sign succeeds; a definite rejection reverses; an uncertain outcome enters reconciliation.

OutcomeDid Gateway call NRS?Billing actionClient response
Wallet debit declinedNoNo successful charge402 or 422 with Wallet debit was declined
Recovery state cannot be persisted after debitNoSafe reversal is started503 with a traceId
NRS returns valid sign successYesCharge remainsNRS success body, requiring HTTP 2xx, body code: 201, and data.ok: true
NRS returns an eligible, structurally valid 4xx rejectionYesReversal is startedSanitized 502 Invoice signing failed with traceId
Timeout, transport failure, NRS 5xx, malformed body, unexpected 2xx, or NRS 408/409/425/429MaybeCharge is retained for reconciliation502 unconfirmed outcome with traceId

The public error is intentionally bounded so dependency details and internal billing identifiers do not leak.

Definitive sign failure
{
  "message": "Invoice signing failed",
  "traceId": "req-018f0f56-f5f5-7c98-a21a-4a03c11a85b1"
}
Unconfirmed sign outcome
{
  "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.

Idempotency key patterns
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-1

Keep the key in the same durable row as your invoice or onboarding command. Never generate it inside a network retry loop.

CommandRequired?Replay behavior
POST /sub-businessesYesnrsCredentials 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-materialYesAn identical store-or-replace request is deduplicated; changed QR material needs a new key
Pass-through POST /api/v1/invoice/signKey recommended; attempt number defaults to 1Keep 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/signYesExact logical document reuses the accepted command; changed content under the key conflicts
POST /invoices/{id}/transmitYesAn 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 resultRetry automatically?Safe action
Represented-business onboarding timed out before completionYes, boundedResend the same optional profile fields with the same idempotency key, and include nrsCredentials while the intent may still be pending
Completed onboarding response was lostYesReplay 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 responseUsually, with bounded backoffRetry the same URL; respect rate limits and your total time budget
Unbilled validation returned a definite document errorNoCorrect the invoice, assign a deliberate command/version, then validate again
Unbilled validation had a transport timeoutYes, boundedRe-send the same document; validation does not create a signing debit
Pass-through sign returned normal NRS successNoStore the response and move to lookup/transmission
Pass-through sign returned 402 or 422No immediate retryRestore balance or account readiness; once failure is conclusive, use a new key and incremented attempt number
Pass-through sign returned 409 replayNoRecover the original response or verify the IRN; do not create random keys to bypass the guard
Pass-through sign returned definitive 502No blind retryKeep the trace, confirm reversal, correct the cause, then use a new key and incremented attempt number
Pass-through sign returned unconfirmed 502NoKeep the original document and traceId; check the IRN authoritatively and contact Duplo support
Stored intake timed out before 202 was receivedYesRe-send the exact command with the same idempotency key
Stored intake returned 202No new intakePoll the returned invoice ID
Stored status is still pendingYes, read onlyPoll with bounded exponential backoff and jitter
Transmission failed or timed outDo not re-signRead 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 statusTypical Gateway meaningFirst action
400Malformed JSON, header, or UUID; an invalid idempotency key; a server-owned field; or a mismatch between the represented business and the body identityCorrect the request; do not retry unchanged
401Missing, invalid, expired, or revoked API key; invalid NRS portal login during onboardingRe-authenticate or rotate the right credential
402 / 422Signing debit declined, or onboarding NRS identity is inactive/incompleteCheck wallet/account or NRS readiness
403The 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 NRSStop; repair the tenant mapping or the NRS link
404Domain is unavailable, the represented business or invoice is hidden or absent, or a supplied NRS TIN did not matchVerify host, state, stored IDs, and NRS registration
409State/mode drift, idempotency conflict, ambiguous active NRS-business selection, Pass-through replay, or operation incompatible with approved modeRead current configuration before another command
413Buffered request exceeds the Gateway limitReduce payload size; do not split one NRS invoice arbitrarily
429Rate limit reachedHonor Retry-After when supplied; otherwise use bounded jittered backoff
502NRS/dependency failure or a sanitized sign rejection/ambiguityRead the message; preserve traceId; never blindly retry sign
503Registry, authorization, billing, or recovery service unavailableRetry 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:

  1. Freeze automatic sign retries for that IRN and represented business.
  2. Persist the exact request bytes, IRN, gatewaySubBusinessId, idempotency key, resolved attempt number (1 when omitted), request time, and returned traceId.
  3. Check your own response/archive first. If the original success was stored elsewhere, use it.
  4. 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.
  5. 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

On this page