Onboard Represented Businesses
Verify each NRS-registered issuer, create its Gateway mapping, and optionally store the material used for invoice QR codes.
A represented business is the legal entity whose verified NRS identity appears on an invoice. It sits under the top-level business you manage in Duplo Dashboard: the top-level business owns the approved domain, API key, wallet, billing, and webhooks, while the represented business owns only its NRS identity, invoice routing, and optional QR material. It gets no Duplo Dashboard login, API key, or wallet of its own. Onboarding one is unbilled; only invoice signing carries a charge.
Prerequisites for each represented business
Confirm all of the following before you send the onboarding request:
- The NRS entity, and at least one business under it, are active in the NRS environment that your API key resolves to. The key carries its own test or live state, and that state alone selects the environment; no header or body field overrides it.
- The business has selected Duplo as its Access Point Provider (APP) in the NRS portal. If it has not, onboarding fails with
403. - You have that business's NRS portal email and password. The Gateway uses them for this one verification request.
- The entity contains exactly one active business, or you know the exact TIN of the one to select.
- That business has an ID, name, TIN, and a non-empty IRN template in NRS.
- You have chosen an
x-idempotency-keyfor the request and have durable storage for the identifiers it returns. See Design stable idempotency keys for naming patterns.
NRS QR material is optional
Omit nrsCryptoKeys when you do not need invoice QR generation yet. The business is onboarded with cryptoVersion: 0, and the absence of these values does not block validation, signing, or transmission. Duplo skips QR generation until both values have been stored. To send the material now, see where it comes from first.
Send the onboarding request
Send the request to your approved domain, replacing acme with your own domain label from Choose a domain label. The request is scoped to the top-level business that owns the API key, so it carries no x-sub-business-id header, and x-idempotency-key is required.
This minimal request is valid when the authenticated NRS entity has exactly one active business:
curl --request POST \
--url https://acme.invoice.tryduplo.com/sub-businesses \
--header 'content-type: application/json' \
--header 'x-api-key: pk_test_replace_with_your_key' \
--header 'x-idempotency-key: onboard-acme-retail-0001' \
--data '{
"nrsCredentials": {
"email": "nrs-owner@acme.example",
"password": "replace-at-runtime"
}
}'One optional field, tin, selects which business to onboard. Omit it when the authenticated NRS entity has exactly one active business, and send it when the entity has more than one, so that it picks out a single issuer:
{
"nrsCredentials": {
"email": "nrs-owner@acme.example",
"password": "replace-at-runtime"
}
}Request-field rules
Duplo verifies the business's identity with NRS and stores what NRS returns. Only nrsCredentials and tin affect which business that is. Every other field below is a local label of your own: it never selects the business and never overrides the NRS-verified values.
| Field | Requirement and use |
|---|---|
nrsCredentials | Required while the onboarding intent is new or pending. Both email and password are required inside the object. A replay of a completed onboarding can omit the object. |
tin | Optional selector, 1 to 255 characters. Matched trimmed and case-insensitively, and it must select exactly one active NRS business. When omitted, the entity must contain exactly one active business. |
name, legalName | Optional local profile values, each limited to 50 characters. |
email | Optional local contact address, validated as an email and limited to 254 characters. This is not the NRS portal login. |
phoneNumber | Optional local contact number in E.164 format, such as +2348012345678, limited to 20 characters. |
invoiceNumberPrefix | Optional local value from 1 to 32 characters. |
nrsCryptoKeys | Optional QR material. When present, both certificate and publicKey must be non-blank strings; the UTF-8 limits are 32 KiB and 16 KiB respectively. |
The Gateway rejects unknown fields with 400. Do not send existingBusinessId, businessId, parentBusinessId, entityId, entityReference, firsBusinessId, nrsBusinessId, businessName, or irnTemplate; those identity and ownership values are server-resolved.
What happens during onboarding
Bind tenant
Gateway resolves the approved host and re-checks on this request that the API key belongs to the business owning it, in the same test/live state.
Create intent
Spend normalizes and records the optional profile fields and the idempotency key before Gateway contacts NRS.
Verify NRS
Gateway authenticates, loads the active entity, and selects one issuer using the optional TIN, or the only active business.
Create mapping
Gateway sends Spend a short-lived signed identity proof plus optional QR material; Spend creates the represented-business record in one step.
The Gateway uses the NRS password only for that verification call. It is not logged, echoed back, stored, or forwarded to Spend. Spend receives only the signed NRS identity proof and, when supplied, the optional QR values.
Selection fails with:
422when no active business is available, the selected business is inactive, or its IRN template is missing.404when a supplied TIN matches no NRS business.409when the entity has several active businesses and no TIN narrows them to one.502when NRS returns incomplete or malformed identity data.
Save the response
A first-time onboarding that sends no QR material returns 201:
{
"onboardingIntentId": "33333333-3333-4333-8333-333333333333",
"gatewaySubBusinessId": "44444444-4444-4444-8444-444444444444",
"status": "completed",
"replayed": false,
"cryptoVersion": 0
}onboardingIntentId identifies this onboarding intent. No endpoint takes it as input, so you do not need it for later invoice calls. gatewaySubBusinessId is the Duplo-issued UUID for the represented business. status is always completed on a 2xx response, so a successful call never leaves an intent for you to poll. replayed is true when the idempotency key matched an earlier completed onboarding and these identifiers came from that first attempt; a replay returns 200 rather than 201.
Store at least this mapping in your integration database:
| Value to store | Where it comes from | Why you need it |
|---|---|---|
| Your own customer or legal-entity ID | Your system | Resolve the represented business from your application |
gatewaySubBusinessId | This response | Sent as x-sub-business-id on invoice operations, and used as the path segment when storing QR material |
NRS business_id | NRS portal or the NRS entity endpoint | Required as business_id in a Pass-through NRS invoice |
| Verified TIN and NRS name | NRS, by way of this onboarding | Must match the issuer identity in invoice payloads |
| IRN template, including its service segment | NRS portal or the NRS entity endpoint | Required to build a valid IRN |
cryptoVersion | This response | 0 means no QR material, 1 is the first stored pair, and later replacements increment it |
Onboarding deliberately does not return the NRS business ID or the IRN template. Read both from the NRS portal, or from the unbilled NRS authentication and entity endpoints described in NRS API context. You can do that before or after onboarding, because these values come from NRS rather than from Duplo.
Keep gatewaySubBusinessId in durable storage. It is the Duplo-issued UUID for this represented business, not its NRS business_id. Send it as x-sub-business-id on represented-business-scoped invoice requests, and use it as the path identifier when adding or replacing that business's QR material. The Gateway has no GET /sub-businesses list and no recovery endpoint, so you cannot read it back if you lose it.
Safe retry
If the request times out while the intent is new or pending, resend the same optional profile fields with the same x-idempotency-key, and include nrsCredentials again. Once onboarding has completed, the same profile and key return the original identifiers with replayed: true, and that replay may omit both nrsCredentials and nrsCryptoKeys. Changing any profile value under the same key returns 409. Do not create a new idempotency key just because the first HTTP response was lost, because a new key starts a second onboarding attempt.
Add or replace optional NRS QR material
You can supply nrsCryptoKeys during onboarding or store it later against the represented-business ID. When the object is present, both values are required.
NRS issues this material, so you do not generate the key pair yourself. In the NRS taxpayer portal, open API Integration, then Manage Cryptographic keys, and choose Generate cryptographic key. Name the key so you can recognize it later, then download the keys file. Despite its .txt extension the file is JSON, and it holds a base64-encoded public_key and certificate. Send certificate exactly as the file gives it. For publicKey, Duplo accepts either that same base64 value or the PEM it decodes to, which NRS extracts with:
jq -r '.public_key' crypto_keys.txt | openssl base64 -d -out public-key.pemcurl --request POST \
--url https://acme.invoice.tryduplo.com/sub-businesses/44444444-4444-4444-8444-444444444444/nrs-crypto-material \
--header 'content-type: application/json' \
--header 'x-api-key: pk_test_replace_with_your_key' \
--header 'x-idempotency-key: qr-material-acme-retail-0001' \
--data '{
"nrsCryptoKeys": {
"certificate": "nrs-issued-certificate-value",
"publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
}'A successful call returns 200, whether this is the first pair stored or a replacement. rotatedAt carries that name in both cases:
{
"gatewaySubBusinessId": "44444444-4444-4444-8444-444444444444",
"cryptoVersion": 1,
"rotatedAt": "2026-08-31T15:20:36.000Z"
}At onboarding and replacement time, the Gateway treats both strings as opaque and stores them exactly as supplied. It does not parse an X.509 certificate, check certificate dates, normalize the values, measure RSA key size, or verify a certificate/key match. The QR generator interprets them only when a signed or received invoice needs a QR code: it embeds certificate in the payload and uses publicKey as plain PEM or base64-encoded PEM for RSA encryption. Unusable material can make QR generation fail, but QR generation is best-effort, and it never turns this material into an onboarding, validation, signing, or transmission requirement.
The endpoint re-checks the top-level business, the approved domain's test/live state, and that the represented business belongs to that top-level business. It sends the opaque values to Spend for storage and makes no NRS call. A represented-business ID that belongs to another account and one that does not exist at all both return the same 404, so the response never reveals whether another account holds that ID. Each successful replacement increments cryptoVersion; an idempotent replay does not.
Continue with Build invoice payloads.
How is this guide?
Last updated on