Skip to Content
NRS E-Invoicing

NRS E-Invoicing

Elyonar is your Access Point Provider (APP) for the Nigeria Revenue Service (NRS) e-invoicing platform. You connect to NRS through Yona Access Point, Elyonar’s API. You send each invoice in the NRS format, and Yona Access Point validates it, has NRS sign it, transmits it to the buyer and tracks it to delivery. You never hold NRS credentials or call NRS yourself.

Base URL
https://nrs.useyona.com

This guide takes you from your first API call to a delivered, paid and reported invoice. For every endpoint, field and error code, see the API reference in the partner portal, available once you sign in.

Partner programme coming soon

The partner portal, where partners sign up and create API credentials, is coming soon. This guide shows how the integration works so you can plan for it.

How it fits together

Your system sends invoices to Yona Access Point, which works with NRS. The partner portal, where you manage your account and credentials, sits outside the invoice path.

  • Your system sends every request in this guide to Yona Access Point.
  • Yona Access Point handles everything with NRS: validation, signing, transmission and delivery.
  • The partner portal (coming soon) is where you sign up, get approved, manage your team and billing, and create API credentials. Your invoices never pass through it.
  • NRS is the tax authority. Elyonar holds the NRS credentials, so the only credentials you need are your Yona Access Point ones.

Before you start

You need three things before your first invoice:

  1. An approved partner account. Sign up on the partner portal (coming soon). You can create API credentials once Elyonar approves your account, and the portal shows anything approval still needs.
  2. A taxpayer that has chosen Elyonar on NRS. For each business you send invoices for, the business enables e-invoicing on the NRS e-invoicing portal , selects Elyonar as its access point and grants the Submit Invoice permission.
  3. That taxpayer’s NRS key file. The business downloads it from its own NRS account. Yona Access Point uses it to generate the QR code printed on each invoice.

Step 1: Create API credentials

In the partner portal, open Credentials and create a credential. You get a client ID, which starts with appng_p_, and a client secret.

Store the secret now

The client secret is shown once and cannot be retrieved later. If you lose it, revoke the credential and create a new one.

Credentials expire after 12 months. To rotate without downtime, create a second credential, switch your system to it, then revoke the old one.

Step 2: Get an access token

Exchange your client ID and secret for an access token. The body is form-encoded, not JSON.

POST /v1/oauth/token

bash
curl -X POST https://nrs.useyona.com/v1/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=appng_p_3f9a1c2b7d4e5f60718293a4" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Send the token on every other request in an Authorization: Bearer <access_token> header. A token lasts one hour and can’t be refreshed: reuse it until it expires, then request a new one.

The examples in this guide keep the token in $TOKEN for cURL and in token for JavaScript. The JavaScript examples use the fetch built into Node.js 18 and later, so they need no extra packages. To stay short, they skip error handling: in your code, check response.ok and read the error described in Errors.

Errors from this endpoint use the OAuth format, such as {"error": "invalid_client"} for an unknown, revoked or expired credential, instead of the error format used everywhere else.

Step 3: Connect a taxpayer

Before you submit invoices for a business, connect its TIN. This creates a grant, your permission to submit invoices for that taxpayer, and Yona Access Point checks it with NRS straight away.

POST /v1/taxpayers

bash
curl -X POST https://nrs.useyona.com/v1/taxpayers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tin": "12345678-0001"}'

A grant stays pending until NRS shows that the business has chosen Elyonar. next_action says what to do next, and stall.actor says who has to act: taxpayer means the business must finish the steps on the NRS portal, and provider means the fix is on Elyonar’s side. To check again, send the same request. It checks NRS again without creating a duplicate, at most once every five minutes, and last_checked_at shows when NRS was last asked.

StatusMeaningCan submit
pendingWaiting for the business to choose Elyonar on NRSNo
connectedConfirmed with NRSYes
staleNRS no longer shows the permission. Reconnects on its own once the business grants it againNo
revokedYou revoked it. Connecting the TIN again creates a new grantNo

List your grants with GET /v1/taxpayers. To disconnect a taxpayer, call DELETE /v1/taxpayers/{id}: new submissions for that TIN are refused straight away, and invoices already accepted are still delivered.

Install the taxpayer’s key

Yona Access Point generates each invoice’s QR code with the business’s own NRS key. The business downloads the key file from My Account → API Integration → Manage Cryptographic Keys in its own NRS account, not the provider account. Upload the file’s contents unchanged.

PUT /v1/taxpayers/{tin}/crypto-key

bash
curl -X PUT https://nrs.useyona.com/v1/taxpayers/12345678-0001/crypto-key \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @crypto_keys.json

The file is small, about 700 bytes, and holds a public_key and a certificate. It contains no private key. Ask the business to use Re-download Key on the NRS portal and not Generate while a key is active, because generating can replace a working key. Uploading again replaces the stored key, and GET /v1/taxpayers/{tin}/crypto-key tells you whether a key is on file without returning it.

Error codeMeaning
CRYPTO_BUNDLE_MALFORMEDThis is not the NRS key file
CRYPTO_BUNDLE_PLATFORM_KEY_UNKNOWNThe file is from a different NRS platform
CRYPTO_BUNDLE_CERTIFICATE_INVALIDThe public key was pasted into the certificate field
CRYPTO_BUNDLE_UNUSABLEThe file looks right but cannot be used to generate a QR code

Step 4: Validate an invoice

Check an invoice against the NRS format and rules before you submit it. Nothing is stored and nothing is sent to NRS. This step is optional, but it catches problems early.

POST /v1/invoices/validate

bash
curl -X POST https://nrs.useyona.com/v1/invoices/validate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @invoice.json

A failing invoice still returns 200, with valid: false and the problems listed in errors. The taxpayer must be connected, as for a real submission.

Step 5: Submit an invoice

POST /v1/submissions

Send the invoice in the NRS format, with two differences:

  • Put invoice_number at the top level, and leave out business_id. Yona Access Point fills in business_id from the taxpayer’s grant, and refuses a payload that includes it.
  • irn is optional. Yona Access Point builds it for you, as described in The IRN and QR code. If you send one, it must match.

Every submission needs an Idempotency-Key header with a unique value per invoice, such as a UUID.

bash
curl -X POST https://nrs.useyona.com/v1/submissions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7d9f2c1e-4b8a-4e3f-9a6d-5c2b1e8f0a47" \
  -d @invoice.json

invoice.json:

{ "invoice_number": "INV001", "issue_date": "2026-09-29", "due_date": "2026-10-29", "invoice_type_code": "381", "invoice_kind": "B2B", "document_currency_code": "NGN", "tax_currency_code": "NGN", "accounting_supplier_party": { "party_name": "Greenfield Technologies Ltd", "tin": "12345678-0001", "email": "accounts@greenfield.example", "postal_address": { "street_name": "32 Owonikoko Street", "city_name": "Ikeja", "postal_zone": "100001", "country": "NG" } }, "accounting_customer_party": { "party_name": "Harbour Foods Ltd", "tin": "87654321-0001", "email": "procurement@harbourfoods.example", "postal_address": { "street_name": "1 Marina Road", "city_name": "Lagos", "postal_zone": "101233", "country": "NG" } }, "invoice_line": [ { "hsn_code": "8471.30", "product_category": "Machinery", "discount_rate": 0, "discount_amount": 0, "fee_rate": 0, "fee_amount": 0, "invoiced_quantity": 2, "line_extension_amount": 50000, "item": { "name": "Network switch", "description": "24-port managed network switch" }, "price": { "price_amount": 25000, "base_quantity": 1, "price_unit": "EA" } } ], "tax_total": [ { "tax_amount": 3750, "tax_subtotal": [ { "taxable_amount": 50000, "tax_amount": 3750, "tax_category": { "id": "STANDARD_VAT", "percent": 7.5 } } ] } ], "legal_monetary_total": { "line_extension_amount": 50000, "tax_exclusive_amount": 50000, "tax_inclusive_amount": 53750, "payable_amount": 53750 } }

202 means the invoice is safely stored as a submission. Yona Access Point then has NRS sign it, transmits it and tracks delivery in the background. Follow its progress in Step 6 or with webhooks.

Retries are safe

Sending the same Idempotency-Key with the same body returns the original 202 and creates nothing new. The same key with a different body is refused with IDEMPOTENCY_KEY_REUSED.

StatusCodeMeaning
400FIELD_NOT_ALLOWEDThe payload included business_id
402ENTITLEMENT_BLOCKEDYour account cannot submit right now. Invoices already accepted still complete
403GRANT_NOT_CONNECTEDThe supplier TIN has no connected grant
403GRANT_STALEThe taxpayer’s permission lapsed on NRS
403PARTNER_SUSPENDEDYour account is suspended
409IRN_CONFLICTThis invoice number and date were already submitted for the taxpayer
413PAYLOAD_TOO_LARGEThe body is larger than 1 MB
422IRN_MISMATCHThe irn you sent does not match the one Yona Access Point built. The response shows both
422IDEMPOTENCY_KEY_REUSEDThe key was already used with a different body
422VALIDATION_FAILEDThe invoice failed validation, or Idempotency-Key is missing

Step 6: Track it to delivery

accepted, registered, transmitted and delivered in sequence, with rejected and parked as exits

A submission moves forward through these states, and delivered is the successful end. If NRS refuses the content, the submission becomes rejected: fix it and retransmit. If delivery keeps failing, it becomes parked until you retransmit.

StateMeaning
acceptedStored by Yona Access Point and on its way to NRS
registeredNRS signed the invoice and registered its IRN
transmittedNRS accepted it for delivery to the buyer
deliveredThe buyer’s side confirmed receipt
rejectedNRS refused the content. state_reason says why
parkedDelivery kept failing. Retransmit to resume

GET /v1/submissions/{id}

bash
curl https://nrs.useyona.com/v1/submissions/0f3c9a6e-5b2d-4e71-8c90-1a2b3c4d5e6f \
  -H "Authorization: Bearer $TOKEN"

To list submissions, newest first, call GET /v1/submissions. Filter by state, or by supplier TIN with taxpayer. Results come in pages: pass next_cursor back to get the next one.

Retransmit

POST /v1/submissions/{id}/retransmit

  • From parked, send no body. The submission resumes where it stopped.
  • From rejected, send {"payload": {...}} with the corrected invoice, following the rules in Step 5. The same submission_id is reused, and the IRN changes only if you changed invoice_number or issue_date.

In any other state, retransmitting returns 409 INVALID_STATE.

Step 7: Check the authority status

Ask NRS directly what it holds for an invoice. This step and the three after it identify the invoice by its full IRN, such as INV001-94ND90NR-20260929. The invoice number on its own returns 404.

GET /v1/invoices/{irn}/authority-status

bash
curl https://nrs.useyona.com/v1/invoices/INV001-94ND90NR-20260929/authority-status \
  -H "Authorization: Bearer $TOKEN"

status is accepted once NRS reports the invoice delivered, and pending before that. It comes from NRS, so for a while it can differ from the submission state in Step 6: an invoice can be transmitted while NRS still reports pending. details shows the rest of what NRS holds, including the payment status and whether the invoice was transmitted and delivered.

This works for invoices submitted through Yona Access Point for your taxpayers. Any other IRN returns 404.

Step 8: Update the payment status

Once NRS has registered an invoice, its content is final. To correct it, issue a credit note (380) or debit note (384) that references it. What you can update is its payment status, and NRS only learns about payments when you report them.

PATCH /v1/invoices/{irn}/payment-status

bash
curl -X PATCH https://nrs.useyona.com/v1/invoices/INV001-94ND90NR-20260929/payment-status \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"payment_status": "PAID", "reference": "TRF-2026-001"}'

payment_status is one of PENDING, PAID, REJECTED or PARTIAL. reference is optional free text for your own payment reference, such as a bank transfer reference. It is not the invoice number: the IRN in the path already identifies the invoice. For a part payment, send PARTIAL with that instalment’s amount, and repeat for each instalment until the invoice is PAID.

nrs_accepted is NRS’s answer. If you send exactly the same update twice in a row, the second one changes nothing: applied is false and NRS is not called again, so retries are safe.

Step 9: File the VAT report

NRS requires every paid invoice to be reported. Once the payment status is PAID, file the report. It is built from the invoice you submitted, so the request needs no body.

POST /v1/invoices/{irn}/report

bash
curl -X POST https://nrs.useyona.com/v1/invoices/INV001-94ND90NR-20260929/report \
  -H "Authorization: Bearer $TOKEN"

report shows exactly what was filed, and nrs_accepted is NRS’s answer. If the invoice is not marked PAID, this returns 409 INVALID_STATE.

Step 10: Download the invoice

Get NRS’s copy of an invoice as JSON, to keep as your record of what NRS holds.

GET /v1/invoices/{irn}/download

bash
curl https://nrs.useyona.com/v1/invoices/INV001-94ND90NR-20260929/download \
  -H "Authorization: Bearer $TOKEN"

The copy is available once NRS has registered the invoice. Before that, this returns 404.

It holds the invoice you submitted, as NRS stores it, with a few fields NRS adds:

FieldWhat it is
business_idThe taxpayer’s NRS business ID, filled in by Yona Access Point from the grant
payment_statusThe latest payment status you reported in Step 8
payment_summaryThe total paid, the balance due, how many payments were reported and when the last one was
payment_eventsEach payment update you reported, with its status, amount, reference and time
id, postal_address_idNRS’s own identifiers for the stored party and address records
TaxCategoryPercentThe tax rate again, as NRS records it. It matches tax_category.percent

Optional fields you did not send come back empty or null.

The IRN and QR code

Every invoice has an Invoice Reference Number (IRN) in the format NRS defines. Yona Access Point builds it from three parts:

PartSourceExample
invoice_numberYour invoice number, using letters and digits onlyINV001
service_idThe taxpayer’s 8-character NRS service ID, from its grant94ND90NR
issue_dateThe invoice date as YYYYMMDD20260929

Joined with hyphens, they give INV001-94ND90NR-20260929. Each IRN must be unique, so use a new invoice_number for every invoice.

Once NRS registers an invoice, its submission includes qr_code, generated with the taxpayer’s own NRS key. Until then, qr_unavailable says why there is no QR code yet: the IRN is not registered, the taxpayer’s key is not on file, or the QR code could not be generated. Print both the IRN and the QR code on every copy of the invoice.

Webhooks

Instead of polling, register an HTTPS endpoint and get notified as your submissions change.

POST /v1/webhook-endpoints

bash
curl -X POST https://nrs.useyona.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://erp.example.com/hooks/nrs"}'
Store the webhook secret now

The secret is shown once. After that, only kcv, a six-character fingerprint of it, is available.

Every notification is a JSON POST in this format:

{ "event_id": "a3c5e7f9-1b2d-4f6a-8c0e-2d4f6a8c0e1b", "event_type": "submission.delivered", "occurred_at": "2026-09-29T09:33:52.480Z", "data": { "submission_id": "0f3c9a6e-5b2d-4e71-8c90-1a2b3c4d5e6f", "irn": "INV001-94ND90NR-20260929", "state": "delivered", "state_reason": null, "taxpayer_tin": "12345678-0001", "environment": "production", "correlation_id": "3e5b7c9d-1f2a-4b6c-8d0e-2f4a6c8e0b1d" } }
EventSent when
submission.acceptedAn invoice was stored
submission.registeredNRS signed it and registered the IRN
submission.transmittedNRS accepted it for delivery
submission.deliveredThe buyer’s side confirmed receipt
submission.rejectedNRS refused the content
submission.parkedDelivery kept failing
grant.updatedA taxpayer grant changed status
invoice.receivedAn invoice arrived for one of your connected taxpayers
operational.noticeMaintenance windows and account notices

Verify the signature

Each notification carries two headers: X-AppNg-Timestamp, in Unix seconds, and X-AppNg-Signature, which is sha256= followed by the hex HMAC-SHA256 of {timestamp}.{raw body}, keyed with your secret. Recompute the signature over the raw bytes you received, compare the two in constant time, and reject any notification whose timestamp is more than five minutes from your clock.

bash
printf '%s.%s' "$X_APPNG_TIMESTAMP" "$RAW_BODY" \
  | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" \
  | awk '{print "sha256=" $2}'

Respond with any 2xx status. Redirects are not followed and count as failures. Failed notifications are retried with exponential backoff, up to eight hours apart.

  • Deduplicate on event_id. The same event can arrive more than once.
  • Do not rely on order. Events can arrive out of sequence, so treat the submission itself, from Step 6, as the source of truth.
  • Rotating the secret takes effect at once. POST /v1/webhook-endpoints/{id}/rotate-secret returns a new secret and the old one stops working immediately, so get your verifier ready for the new secret first.

You can also manage endpoints on the Webhooks page of the partner portal.

Receiving invoices

Invoices that other access points send to your connected taxpayers appear at GET /v1/inbound-invoices, newest first, and each one also triggers an invoice.received notification. The list shows summaries: fetch GET /v1/inbound-invoices/{id} for the full invoice as NRS delivered it.

Invoice fields

These are the fields Yona Access Point checks. Any other field in the NRS format is accepted and passed to NRS unchanged.

Top-level fields

FieldRequiredNotes
invoice_numberYesLetters and digits only. Becomes part of the IRN
issue_dateYesYYYY-MM-DD
invoice_type_codeYesSee Reference data
invoice_kindYesB2B, B2C, B2G or G2B
document_currency_codeYesISO 4217, for example NGN
tax_currency_codeYesISO 4217
accounting_supplier_partyYesThe seller. See Parties
invoice_lineYesAt least one line
legal_monetary_totalYesThe invoice totals
accounting_customer_partyNoThe buyer. Can be left out for B2C
due_dateNoYYYY-MM-DD
issue_timeNoHH:mm:ss
tax_point_dateNoYYYY-MM-DD
tax_totalNoThe tax breakdown
billing_referenceCredit and debit notesThe original invoice’s irn and issue_date
noteNoFree text
irnNoBuilt by Yona Access Point. If you send it, it must match
business_idNeverFilled in by Yona Access Point from the grant

Parties

accounting_supplier_party and, when present, accounting_customer_party:

FieldRequiredNotes
party_nameYesLegal business name
tinYesTax Identification Number
emailYesContact email
postal_address.street_nameYes
postal_address.city_nameYes
postal_address.postal_zoneYes
postal_address.countryYesISO 3166-1, for example NG
postal_address.stateNoISO 3166-2, for example NG-LA
postal_address.lgaNoLocal government area
telephoneNo
business_descriptionNo

Lines

FieldRequiredNotes
invoiced_quantityYes
line_extension_amountYesThe line total before tax
item.nameYes
item.descriptionYes
price.price_amountYesPrice per base quantity
price.base_quantityYes
price.price_unitYesA quantity code such as EA, from invoice-quantity-codes
discount_rate, discount_amountYesUse 0 when none apply
fee_rate, fee_amountYesUse 0 when none apply
hsn_code, product_categoryNoClassification for goods
isic_code, service_categoryNoClassification for services

Totals

legal_monetary_total needs line_extension_amount, tax_exclusive_amount, tax_inclusive_amount and payable_amount. When you send tax_total, each entry needs tax_amount, and each tax_subtotal needs taxable_amount, tax_amount and a tax_category with its id and percent. Yona Access Point checks that the amounts add up before anything is sent to NRS.

Reference data

GET /v1/resources/{type} returns NRS’s code lists: countries, currencies, tax-categories, payment-means, invoice-types, services-codes, vat-exemptions, hs-codes and invoice-quantity-codes. Any valid token can read them. If NRS cannot be reached, you get the last known list with stale: true.

The invoice types you will use most:

CodeTypeNotes
381Commercial InvoiceA standard sales invoice
380Credit NoteNeeds billing_reference
384Debit NoteNeeds billing_reference
386Factored Invoice

The VAT categories:

CodeRate
STANDARD_VAT7.5%
REDUCED_VAT7.5%
ZERO_VAT0%
EXEMPTED0%

NRS publishes other tax categories as well, such as WITHHOLDING_TAX and STAMP_DUTY. Read the full list from tax-categories.

Errors

Every endpoint except the token endpoint returns errors in this format:

{ "code": "GRANT_NOT_CONNECTED", "message": "No connected grant for this taxpayer", "details": { "tin": "12345678-0001" } }

Branch on code, which is stable, rather than on message. details adds specifics when there are any, such as the fields that failed or the IRN Yona Access Point expected.

StatusCodeMeaning
401UNAUTHENTICATEDMissing, invalid or expired token. Request a new one
402ENTITLEMENT_BLOCKEDYour account cannot submit new invoices
403INSUFFICIENT_SCOPEThe token does not allow this action
403CROSS_PARTNER_FORBIDDENThe resource belongs to another partner
404NOT_FOUNDUnknown resource
409INVALID_STATEThe action does not apply in the resource’s current state
422VALIDATION_FAILEDThe request failed validation. details names the problem
429RATE_LIMITEDToo many requests. Slow down and retry

The API reference in the partner portal (coming soon) lists every error code.