API Reference

Avtax ETA API Documentation

The Avtax ETA API is a B2B gateway for integrating with the Egyptian Tax Authority (ETA). It handles the full document lifecycle — from JSON input to a cryptographically signed, ETA-compliant document — so your team can focus on business logic, not tax compliance.

Base URL: All API requests are made to https://eta-api.avtax.net. For interactive exploration, visit the Scalar API Reference.

1

Register as a Partner

Contact us to receive your partner credentials. You'll get an email and password to authenticate and receive a JWT token for all API calls.

2

Register Your Customers

Use the Customer API to register each of your clients with their ETA credentials. We securely validate and cache their ETA access tokens automatically.

3

Submit Documents

Send a simple JSON payload per customer. We validate, serialize, and cryptographically sign it using the customer's registered desktop signing app, then submit it to the ETA.


Authentication

The Avtax ETA API uses two authentication schemes depending on which API group you are calling.

Partner API
JWT Bearer Token
Used for Customer management and dashboard endpoints. Obtain via /api/Auth/login.
Authorization: Bearer <token>
Document API
X-Api-Key Header
Used for Invoice and Receipt submission endpoints. Generated per-customer via the Customer API.
X-Api-Key: <customer-api-key>

Obtaining a JWT Token (Partner API)

HTTP
POST /api/Auth/login
Content-Type: application/json

{
  "email": "partner@yourcompany.com",
  "password": "your-password"
}
Response
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiration": "2026-02-01T00:00:00Z",
  "message": "Login successful"
}

Using the X-Api-Key (Document API)

Each customer has a unique API key generated via POST /api/Customers/{id}/api-keys. Provide it in the X-Api-Key header for all Invoice and Receipt requests.

HTTP Header
X-Api-Key: a1b2c3d4e5f6...

Quick Start — Submit an Invoice

The following example submits a simple B2B invoice for a registered customer. Use the customer's X-Api-Key (from POST /api/Customers/{id}/api-keys) in the request header.

JSON
POST /api/Invoices/submit
X-Api-Key: <customer-api-key>
Content-Type: application/json

{
  "invoices": [
    {
      "internalID": "INV-2026-0001",
      "dateTimeIssued": "2026-01-15T10:00:00Z",
      "documentType": "I",
      "taxpayerActivityCode": "4620",
      "receiver": {
        "type": "B",
        "id": "012345678",
        "name": "Client Company LLC"
      },
      "invoiceLines": [
        {
          "description": "Software Development Services",
          "itemType": "EGS",
          "itemCode": "EG-123456789-12345",
          "unitType": "EA",
          "quantity": 1,
          "unitValue": {
            "currencySold": "EGP",
            "amountEGP": 10000
          }
        }
      ]
    }
  ]
}

Automatic calculations: You only need to provide line item prices. Avtax ETA API automatically calculates all totals, VAT (14%), withholding taxes, and other ETA-required fields according to the latest ETA specifications.


Partner Auth

Manage partner account registration and login. All endpoints in this group return a JWT token used to authenticate Partner API calls.

POST /api/Auth/register

Create a new partner account. Returns a JWT token on success.

POST /api/Auth/login

Authenticate with email and password. Returns a JWT Bearer token and its expiration date.

Register — Request Body
{
  "partnerName": "My Integration Co.",
  "email": "partner@example.com",
  "password": "SecurePassword1!",
  "phoneNumber": "+201000000000"
}

Customer Management

Register and manage customer ERPs under your partner account. All endpoints require a Authorization: Bearer <token> header. Each customer gets an isolated API key used for document submission.

GET /api/Customers

List all customers registered under your partner account, including their active API key and signature client connection status.

GET /api/Customers/{id}

Retrieve a single customer by ID.

POST /api/Customers

Register a new customer ERP with their ETA credentials. The Tax ID is automatically extracted from the ETA token and verified — no need to supply it manually.

PUT /api/Customers/{id}

Update a customer's details or ETA credentials. If new credentials are supplied, they are validated against the ETA and must match the customer's registered Tax ID.

DELETE /api/Customers/{id}

Permanently remove a customer ERP and all associated data from your account.

GET /api/Customers/dashboard

Get a summary of your customer base: total, active, and currently connected signature clients.

API Key Management

Each customer requires an active API key to submit documents. Generating a new key automatically revokes the previous one.

POST /api/Customers/{id}/api-keys

Generate a new API key for the customer, revoking any previously active keys. Returns the updated customer info including the new key.

DELETE /api/Customers/{id}/api-keys

Revoke the customer's active API key without generating a replacement. Returns 400 if no active key exists.

ETA Notification Forwarding

Avtax ETA API acts as a relay between ETA and your customer's ERP system. ETA calls our callback URLs, we receive the notification, transform it into a simple JSON format, and POST it to the URL your customer registered.

How it works
  1. You register the customer and set their ERP Callback URL (the endpoint in their own system).
  2. You register https://eta-api.avtax.net/{customerApiKey} as the callback base URL inside ETA portal for that customer.
  3. ETA calls PUT /{customerApiKey}/notifications/documents or PUT /{customerApiKey}/notifications/receipts on our server.
  4. We validate the key, translate the payload, and POST a simplified notification to the customer's ERP Callback URL.
  5. The customer's ERP returns 200 OK and processes the update.
PUT /{customerApiKey}/notifications/documents

ETA calls this when a document event occurs (validated, received, rejected, cancelled). We forward a simplified POST to the customer's ERP Callback URL.

PUT /{customerApiKey}/notifications/receipts

ETA calls this when a receipt event occurs (validated, received, cancelled). We forward a simplified POST to the customer's ERP Callback URL.

What your customer's ERP receives

Your customer's ERP endpoint receives a POST request with this simplified JSON body. They must return 200 to acknowledge receipt.

Outbound Notification Payload (JSON)
{
  "source": "ETA",
  "type": "document",
  "deliveryId": "838847HS2",
  "receivedAt": "2026-01-15T10:00:00Z",
  "notifications": [
    {
      "eventType": "document-validated",
      "uuid": "F9D425P6DS7D8IU",
      "internalId": "INV-2026-0001",
      "submissionId": "JU7GH07JNA23N",
      "status": "Valid"
    }
  ]
}
type
"document" or "receipt"
eventType
ETA event (e.g. document-validated, Validated, Cancelled)
status
Valid, Invalid, Cancelled, Rejected
Create Customer — Request Body
{
  "companyName": "My Client LLC",
  "branchCode": "0",
  "etaClientId": "your-eta-client-id",
  "etaClientSecret": "your-eta-client-secret",
  "isProduction": false
}

Invoice Submission

Submit standard invoices, credit notes, and debit notes to the Egyptian Tax Authority. All document types flow through a single endpoint using the documentType field. All endpoints require an X-Api-Key header.

Signature App required: Invoice signing requires the customer's USB token to be connected via the Signature Desktop App. Download Signature App

POST /api/Invoices/submit

Submit one or more invoices. Supports: I (Invoice), C (Credit Note), D (Debit Note).

PUT /api/Invoices/cancel

Request cancellation of a previously submitted document. Provide the document uuid and a cancellation reason.

PUT /api/Invoices/reject

Reject a document received from a supplier. Provide the document uuid and a rejection reason.

PUT /api/Invoices/decline-rejection

Decline a rejection request received for one of your submitted documents. Provide the document uuid.

GET /api/Invoices/search

Search submitted or received documents using filters: date range, direction, status, type, receiver/issuer ID, internal ID, UUID, and pagination.

GET /api/Invoices/{uuid}/details

Retrieve full validation details and the complete document JSON for a specific invoice.

GET /api/Invoices/{uuid}/pdf

Download the ETA-generated PDF for a specific invoice. Returns a binary PDF file.

GET /api/Invoices/submissions/{uuid}

Retrieve the processing status and individual document results for a specific batch submission UUID.

Document Types

Type "I"
Standard Invoice
B2B sales invoice
Type "C"
Credit Note
Requires original UUID
Type "D"
Debit Note
Requires original UUID

Receipt Submission

Submit B2C sales and return receipts from your POS. You send the business data; the API calculates every total and the receipt UUID, links each receipt to the previous one from the same device, and makes resending safe. All endpoints require an X-Api-Key header.

POST /api/Receipts/submit

Submit one or more receipts. Supports S (Sale) and R (Return). Totals, the receipt UUID and device chain linking are handled by the API. Safe to resend: a receipt that already reached ETA is never registered twice.

GET /api/Receipts/status?deviceSerialNumber={serial}&receiptNumber={number}

Check whether a receipt reached ETA without resending it. Returns accepted, pending, unknown, rejected or not_found, plus the UUID, public link and totals once ETA has accepted it. Add &receiptType=S or R when a return receipt reuses its sale's number. See Checking a receipt without resending it.

GET /api/Receipts/search

Search submitted receipts using filters: issue date range, received date range, POS serial number, receipt number, UUID, and pagination. Receipt number and UUID match exactly and cover receipts ETA received in the last month. A search that matches nothing returns an empty result.

GET /api/Receipts/{uuid}/details

Retrieve a receipt registered with ETA: its UUID, status and total, plus the full receipt as ETA holds it. Returns 404 for a receipt ETA hasn't registered, including one it marked invalid.

GET /api/Receipts/{uuid}/raw

Same as /details, plus rawDocument: the receipt document exactly as ETA received and stored it, as a JSON string.

GET /api/Receipts/submissions/{uuid}

Retrieve full processing details and individual receipt statuses for a specific batch submission UUID.

GET /api/Receipts/submissions/{submissionUuid}/receipts/{receiptUuid}/status

Check whether one receipt from a submission is valid, invalid, rejected_with_submission or still pending, and why. invalid means the receipt failed ETA validation itself (ETA's errors are in issues); rejected_with_submission means it passed, but wasn't registered because another receipt in the same submission failed.

Automatic device chain linking: ETA requires each receipt to reference the UUID of the previous receipt from the same POS device. The API tracks this per seller.deviceSerialNumber. Leave out header.previousUUID (or send "") and the API fills it from the last receipt ETA accepted for that device. For the first receipt from a new device, an empty string is sent, as ETA requires.

Totals and Discounts

Send what the cashier rang up. The API works out every amount ETA needs, to ETA's calculation rules. Don't send line totals, tax amounts, receipt totals or the UUID.

Your POS sends
  • header.receiptNumber and header.dateTimeIssued (UTC)
  • seller, including seller.deviceSerialNumber
  • Each line: quantity, unitPrice before tax, and its tax codes and rates in taxableItems
  • Discounts, each as an amount or a rate
  • Optional: the totals printed on the receipt, in expectedTotals
The API calculates
  • The amounts on each line, including its tax
  • totalSales, netAmount, taxTotals and totalAmount
  • The discount totals
  • The receipt UUID and header.previousUUID
  • All of it comes back in totals when ETA accepts the receipt

Discounts

There are four discount fields. Pick one by what the discount applies to and whether it should lower the tax.

Field Applies to Tax A rate is a percentage of
items[].commercialDiscount One line Before tax: lowers VAT and fees The line value (quantity × unit price)
items[].itemDiscount One line After tax: VAT and fees unchanged. Withholding tax (T4) on the line goes down. The line value after before-tax discounts
receiptDiscount Whole receipt Before tax: lowers VAT and fees The receipt value after before-tax line discounts
extraReceiptDiscountData Whole receipt (a list) After tax: VAT and fees unchanged The receipt value after before-tax discounts, excluding tax

Each discount is an object with an amount or a rate, plus a description of up to 50 characters that appears on the ETA receipt. Send one of the two, not both: amount for a fixed discount, or rate on its own for a percentage, and the API works out the amount. A rate must be between 0 and 100.

Receipt discount: ETA records before-tax discounts line by line, so the API spreads receiptDiscount across the lines. The parts always add up to the amount you sent, or to the amount your rate works out to.

If a discount can't be applied, for example a negative amount, a rate outside 0 to 100, an amount with more decimal places than ETA accepts, or a discount larger than what it applies to, the receipt isn't sent. Its error code is VALIDATION_ERROR and details names each field to correct, for example items[1].itemDiscount or receiptDiscount.

Example: restaurant receipt

A dine-in bill with 14% VAT (T1/V009) on food and drinks, one VAT-exempt line (T1/V003 at rate 0), a 12% service charge (T9/SC01) on every line, a fixed lunch-offer discount on one line and a 10% loyalty discount on the whole bill. expectedTotals holds the totals printed on the bill.

JSON
POST /api/Receipts/submit
X-Api-Key: <customer-api-key>
Content-Type: application/json

{
  "receipts": [
    {
      "paymentMethod": "C",
      "receiptType": "S",
      "header": {
        "dateTimeIssued": "2026-10-13T19:45:00Z",
        "receiptNumber": "T12-1001",
        "currency": "EGP"
      },
      "seller": {
        "rin": "123456789",
        "companyTradeName": "Nile View Restaurant",
        "branchCode": "0",
        "branchAddress": {
          "country": "EG",
          "governate": "Cairo",
          "regionCity": "Zamalek",
          "street": "26th of July St.",
          "buildingNumber": "12"
        },
        "deviceSerialNumber": "POS-0001",
        "activityCode": "5610"
      },
      "buyer": { "type": "P" },
      "items": [
        {
          "internalCode": "MG-01",
          "description": "Mixed grill",
          "itemType": "EGS",
          "itemCode": "EG-123456789-10001",
          "unitType": "EA",
          "quantity": 1,
          "unitPrice": 200,
          "commercialDiscount": { "amount": 20, "description": "Lunch offer" },
          "taxableItems": [
            { "taxType": "T1", "subType": "V009", "rate": 14 },
            { "taxType": "T9", "subType": "SC01", "rate": 12 }
          ]
        },
        {
          "internalCode": "LM-02",
          "description": "Lemon mint",
          "itemType": "EGS",
          "itemCode": "EG-123456789-10002",
          "unitType": "EA",
          "quantity": 2,
          "unitPrice": 35,
          "taxableItems": [
            { "taxType": "T1", "subType": "V009", "rate": 14 },
            { "taxType": "T9", "subType": "SC01", "rate": 12 }
          ]
        },
        {
          "internalCode": "BR-03",
          "description": "Baladi bread",
          "itemType": "EGS",
          "itemCode": "EG-123456789-10003",
          "unitType": "EA",
          "quantity": 2,
          "unitPrice": 15,
          "taxableItems": [
            { "taxType": "T1", "subType": "V003", "rate": 0 },
            { "taxType": "T9", "subType": "SC01", "rate": 12 }
          ]
        }
      ],
      "receiptDiscount": { "rate": 10, "description": "Loyalty card" },
      "expectedTotals": {
        "totalSales": 300.00,
        "netAmount": 252.00,
        "taxTotals": [
          { "taxType": "T1", "amount": 35.28 },
          { "taxType": "T9", "amount": 30.24 }
        ],
        "totalAmount": 317.52
      }
    }
  ]
}

Matching your printed receipt

Send the totals printed on the receipt in expectedTotals. Send only the ones you print; each one you send is checked. We recommend it for every receipt: ETA then never records amounts that differ from what the customer paid.

Field What it is
totalSales All lines before any discount (quantity × unit price)
netAmount All lines after before-tax discounts, excluding tax
taxTotals One entry per tax type, for example T1 for VAT and T9 for service charge
totalAmount The final amount the customer pays

The API compares them with the amounts it calculates for ETA, allowing only for small rounding differences. If any of them differ, that receipt isn't sent: its error code is TOTALS_MISMATCH and details shows the value you sent next to the value ETA would receive. Other receipts in the same request are not affected.

Response — Totals Mismatch
{
  "success": false,
  "acceptedCount": 0,
  "rejectedCount": 1,
  "alreadySubmittedCount": 0,
  "message": "Accepted: 0, Rejected: 1",
  "acceptedReceipts": [],
  "errors": [
    {
      "code": "TOTALS_MISMATCH",
      "message": "Receipt T12-1001 wasn't sent: the totals you sent don't match the amounts calculated for ETA.",
      "field": "T12-1001",
      "details": "totalAmount: you sent 317.25, ETA will receive 317.52. Check the quantities, discounts and tax rates on each line, then resend.",
      "severity": "error"
    }
  ]
}

Every accepted receipt comes back with totals: the exact amounts registered with ETA. Store them with the sale if you reconcile with ETA later. Each receipt also carries its own submissionUUID; keep that one. The top-level submissionUUID covers only the receipts sent to ETA in this request, and is left out when nothing new was sent.

Response — Accepted
{
  "success": true,
  "submissionUUID": "J7XK4QH2B1MZ9V3R6TYD8W5NC0",
  "acceptedCount": 1,
  "rejectedCount": 0,
  "alreadySubmittedCount": 0,
  "message": "Accepted: 1, Rejected: 0",
  "acceptedReceipts": [
    {
      "uuid": "96ae5203423f389f22a1a041e9c59a83c448a19b8b42d914fc725db734189dcd",
      "longId": "M4TQ0WZ3X8D6JRH9KQ2C1YFG10NB7S5P",
      "receiptNumber": "T12-1001",
      "publicLink": "https://invoicing.eta.gov.eg/receipts/search/96ae5203423f389f22a1a041e9c59a83c448a19b8b42d914fc725db734189dcd/share/2026-10-13T19:45Z",
      "submissionUUID": "J7XK4QH2B1MZ9V3R6TYD8W5NC0",
      "alreadySubmitted": false,
      "totals": {
        "totalSales": 300,
        "totalCommercialDiscount": 48,
        "totalItemsDiscount": 0,
        "netAmount": 252,
        "taxTotals": [
          { "taxType": "T1", "amount": 35.28 },
          { "taxType": "T9", "amount": 30.24 }
        ],
        "extraReceiptDiscount": 0,
        "totalAmount": 317.52
      }
    }
  ],
  "errors": []
}

Resending Receipts Safely

Connections drop, and a POS can't always tell whether ETA got a receipt. Resend it: the API makes sure ETA never registers the same receipt twice.

A receipt is identified by seller.deviceSerialNumber, receiptType and header.receiptNumber. Letter case and trailing spaces are ignored when the device serial number and receipt number are compared. Receipt numbers must be unique per device: don't reuse a number for a different sale, and send each number only once per request. A return receipt may reuse the number of the sale it returns; send the two in separate requests. If your numbering restarts (for example every day), a receipt issued more than 24 hours after an accepted receipt with the same number is treated as a new receipt.

To tell a resend from a changed receipt, the API compares every field of the receipt except header.previousUUID and expectedTotals.

When you resend a receipt that The API You get
ETA already accepted Doesn't send it again. The receipt in acceptedReceipts with alreadySubmitted: true and its original uuid, longId, publicLink and submissionUUID. It also counts in alreadySubmittedCount.
Didn't get a clear answer from ETA on an earlier attempt (for example the connection dropped) Checks with ETA first. If ETA has it, it's returned as already accepted. If ETA doesn't, the identical receipt is sent again once a couple of minutes have passed since the earlier attempt. A changed version is held back until it's clear ETA never got the earlier attempt: at most 30 minutes after the last attempt that went unanswered. If ETA keeps refusing the identical receipt, you get ETA's reason as ETA_REJECTION from then on. The normal result, or RECEIPT_OUTCOME_UNKNOWN while ETA can't confirm it yet.
ETA accepted, but you changed its content Doesn't send it, unless it was issued more than 24 hours after the accepted receipt (then it's a new receipt). RECEIPT_ALREADY_SUBMITTED, with the original UUID in details.
ETA accepted, then marked invalid Sends it as the replacement for the invalid receipt, the way ETA requires. If ETA rejected the whole submission because of another receipt in it and didn't keep this one, sends it again in its original place in the device chain. Later receipts from the device are not affected. The receipt in acceptedReceipts. A replacement has referenceOldUUID set to the invalid receipt's UUID. RECEIPT_OUTCOME_UNKNOWN if ETA can't be asked about it right now.
ETA rejected Sends it again. The normal result. Fix the receipt first if ETA said what was wrong.
Another request is sending right now Doesn't send it. RECEIPT_IN_PROGRESS.

"Not yet" is not "failed": RECEIPT_IN_PROGRESS and RECEIPT_OUTCOME_UNKNOWN mean the outcome isn't settled. Keep the receipt queued and resend it unchanged in a couple of minutes.

One invalid receipt holds back its whole request: ETA validates receipts after accepting them. When it finds any receipt in a submission invalid, it rejects the whole submission, so the other receipts sent in the same request aren't registered either. GET /api/Receipts/status shows the receipt that failed as invalid (resend it corrected) and the others as rejected_with_submission (resend them unchanged), all with their original numbers. To keep one bad receipt from holding back others, send receipts as they're issued; batches are fine for clearing a queue.

Recommended offline queue

1

Queue the receipt before you send it

Save each receipt in a local queue with a receipt number that is unique for the device. Send queued receipts in the order they were issued.

2

Send it

Call POST /api/Receipts/submit. When the receipt is in acceptedReceipts, save its uuid, its publicLink (for the QR code) and its own submissionUUID (to check ETA's validation result later), then remove it from the queue. Allow at least 2 minutes before your POS treats the request as timed out. If it times out anyway, step 3 keeps the receipt safe.

3

On a timeout, keep it queued

If the request times out or the connection drops, leave the receipt in the queue exactly as it is. Don't change its content, its issue time or its number.

4

Resend the same receipt unchanged

If ETA already has it, you get alreadySubmitted: true with the original UUID and link. For RECEIPT_IN_PROGRESS or RECEIPT_OUTCOME_UNKNOWN, keep it queued and try again in a couple of minutes.

5

Change a receipt only when the error says it's wrong

Fix the receipt for TOTALS_MISMATCH, ETA_REJECTION or VALIDATION_ERROR, then resend it with the same receipt number. For any other code, follow Receipt Error Codes.

6

Check ETA's validation, or check without resending

ETA validates a receipt shortly after accepting it. To find out where a receipt stands, including whether ETA marked it invalid, call GET /api/Receipts/status. It also helps after the POS restarts.

Send within 24 hours: ETA accepts a receipt up to 24 hours after its issue time (header.dateTimeIssued, in UTC and never in the future). Clear your queue well before then. After 24 hours, ETA only accepts the receipt under a late-submission request raised with ETA first.

Checking a receipt without resending it

Pass the device serial number and receipt number you sent. Both are required; if one is missing, the API answers 400 with the code MISSING_REQUIRED. When a return receipt reuses its sale's number, add receiptType=S or receiptType=R to choose which one you mean; without it you get the one the API sent or checked most recently. The API checks with ETA before it answers, so the status includes ETA's validation result.

HTTP
GET /api/Receipts/status?deviceSerialNumber=POS-0001&receiptNumber=T12-1001
X-Api-Key: <customer-api-key>
Response
{
  "deviceSerialNumber": "POS-0001",
  "receiptNumber": "T12-1001",
  "receiptType": "S",
  "status": "accepted",
  "uuid": "96ae5203423f389f22a1a041e9c59a83c448a19b8b42d914fc725db734189dcd",
  "longId": "M4TQ0WZ3X8D6JRH9KQ2C1YFG10NB7S5P",
  "publicLink": "https://invoicing.eta.gov.eg/receipts/search/96ae5203423f389f22a1a041e9c59a83c448a19b8b42d914fc725db734189dcd/share/2026-10-13T19:45Z",
  "submissionUUID": "J7XK4QH2B1MZ9V3R6TYD8W5NC0",
  "dateTimeIssued": "2026-10-13T19:45:00",
  "lastUpdated": "2026-10-13T19:45:04",
  "totals": {
    "totalSales": 300, "totalCommercialDiscount": 48, "totalItemsDiscount": 0, "netAmount": 252,
    "taxTotals": [ { "taxType": "T1", "amount": 35.28 }, { "taxType": "T9", "amount": 30.24 } ],
    "extraReceiptDiscount": 0, "totalAmount": 317.52
  }
}
Status Meaning What to do
accepted ETA received the receipt and hasn't marked it invalid. The response includes its UUID, public link, submission UUID and totals. The reason field says when ETA is still validating it. Nothing to send.
invalid The receipt failed ETA validation and isn't registered. issues lists ETA's errors. Correct it, then resend it with the same number. It is registered as the replacement.
rejected_with_submission The receipt's own data passed validation, but another receipt sent in the same request failed, so ETA rejected the whole submission and didn't register this one either. Resend it unchanged, with the same number.
pending The receipt is being sent to ETA right now. Check again in a few seconds.
unknown ETA's answer didn't confirm the receipt, for example because the connection dropped. Resend the receipt unchanged. It won't be duplicated.
rejected ETA rejected the receipt or never received it. The reason field says why. Fix the receipt if needed, then resend it.
not_found No receipt with this number has been sent to ETA through the API for this device. Receipts refused before sending, for example for a totals mismatch, are not recorded. Send it. Check the device serial number if you expected a match.

ETA Notifications

Avtax ETA API acts as a relay for ETA event notifications. We receive raw ETA callbacks on behalf of your customers, transform them into a clean JSON format, and forward them directly to each customer's registered ERP endpoint — so your customers never need to expose an ETA-compatible server themselves.

Setup — 3 Steps

1

Register the customer's ERP Callback URL

When creating or editing a customer, provide the URL of their ERP endpoint (e.g. https://customer-erp.com/eta-notifications). We will POST simplified notifications to this address.

2

Register our callback base URL at the ETA portal

In the ETA taxpayer portal, set the callback base URL to:
https://eta-api.avtax.net/{customerApiKey}
Replace {customerApiKey} with the customer's active API key (visible on the Customers page). ETA will automatically append /notifications/documents and /notifications/receipts.

3

Implement the endpoint in the customer's ERP

The customer's ERP must expose a POST endpoint at the registered URL. It should accept the simplified payload below and return 200 OK. No special headers or authentication are required — the URL itself acts as the shared secret.

Outbound Notification Payload

This is what the customer's ERP endpoint receives for both document and receipt events. The type field distinguishes between the two.

Document Event
POST https://customer-erp.com/eta-notifications
Content-Type: application/json

{
  "source": "ETA",
  "type": "document",
  "deliveryId": "838847HS2",
  "receivedAt": "2026-01-15T10:00:00Z",
  "notifications": [
    {
      "eventType": "document-validated",
      "uuid": "F9D425P6DS7D8IU",
      "internalId": "INV-2026-0001",
      "submissionId": "JU7GH07JNA23N",
      "status": "Valid"
    }
  ]
}
Receipt Event
POST https://customer-erp.com/eta-notifications
Content-Type: application/json

{
  "source": "ETA",
  "type": "receipt",
  "deliveryId": "929157AB1",
  "receivedAt": "2026-01-15T11:30:00Z",
  "notifications": [
    {
      "eventType": "Validated",
      "uuid": "9d74ecc5a844354864c762c0d413ee42336561f5",
      "internalId": "REC-2026-0042",
      "submissionId": "BC6J9M86WWVP567DEYGFTD8G10",
      "status": "Invalid"
    }
  ]
}

Field Reference

Field Type Description
source String Always "ETA"
type String "document" or "receipt"
deliveryId String Unique batch ID from ETA, useful for deduplication
receivedAt DateTime UTC timestamp when we received the batch from ETA
notifications Array List of notification items in this batch
eventType String ETA event: document-validated, document-received, document-rejected, document-cancelled, Validated, Received, Cancelled
uuid String Unique document or receipt UUID assigned by ETA
internalId String Your internal document ID as submitted
submissionId String The batch submission UUID
status String Valid, Invalid, Cancelled, or Rejected

Reliability: If the customer's ERP returns a non-200 response or is unreachable, we log the failure but still return 200 to ETA to prevent unnecessary retries. Ensure your ERP endpoint is always available and responds within 15 seconds.


Error Codes

All errors return a consistent JSON structure with success, message, errorCode, and an errors array.

HTTP Status Codes

Status Meaning Common Cause
400 Bad Request Invalid request body, missing fields, or a business rule such as the customer limit or missing ETA credentials
401 Unauthorized Missing or expired JWT, wrong login details or a deactivated partner account, or an X-Api-Key that is missing, revoked or belongs to a deactivated customer
403 Forbidden Signed in, but your account can't use this endpoint (for example an admin-only endpoint)
404 Not Found Customer or document not found under your account
500 Server Error Unexpected error — contact support with your request ID

Internal Error Codes (errorCode field)

Code Description
CUSTOMER_INACTIVE The customer account is deactivated. Re-activate via PUT /api/Customers/{id}.
MISSING_CONFIGURATION The customer has no ETA credentials configured. Update via PUT /api/Customers/{id}.
ETA_AUTH_ERROR The API couldn't sign in to ETA with the customer's credentials, so nothing was sent. Check clientId and clientSecret, then resend.

Receipt Error Codes (errors[].code)

POST /api/Receipts/submit answers 200 once it has processed the request, even when some receipts weren't accepted, so check success and errors. A receipt refused on its own has an entry in errors with its receipt number in field. When the whole request failed, errorCode is set and errors also has an entry for it without a receipt number in field (for ETA_DUPLICATE_PAYLOAD, each affected receipt has its own entry instead). That outcome applies to every receipt that isn't in acceptedReceipts and has no entry of its own. Receipts settled before the failure keep their own result, so one response can hold accepted receipts, per-receipt entries and a request-level error.

Code What happened What to do
TOTALS_MISMATCH The totals in expectedTotals don't match the amounts calculated for ETA beyond small rounding differences, so the receipt wasn't sent. details lists each total that differs, with the value you sent and the value ETA would receive. Compare the quantities, discounts and tax rates on each line with your POS, correct the receipt, and resend it.
RECEIPT_ALREADY_SUBMITTED ETA already accepted a receipt with this device serial number, receipt type and receipt number, and the receipt you sent has different content. An accepted receipt can't be changed, so it wasn't sent. details includes the original UUID. If this is a resend, send the original receipt unchanged, including its issue time. To reverse the sale, issue a return receipt (type R). For a new sale, use a new receipt number.
RECEIPT_IN_PROGRESS Another request is sending this receipt, or another receipt from the same request, to ETA right now. To avoid sending anything twice, this request didn't send it. Wait a few seconds, then resend it or call GET /api/Receipts/status. It won't be duplicated.
RECEIPT_OUTCOME_UNKNOWN The API couldn't confirm whether ETA registered the receipt: the connection dropped, ETA had a server error, ETA's reply couldn't be read or didn't mention the receipt, or ETA hasn't confirmed an earlier attempt yet. Can also be returned in errorCode for the whole request. While an earlier attempt is unconfirmed, a changed version of the receipt isn't sent either, for at most 30 minutes after the last attempt that went unanswered. Keep the receipt queued and resend it unchanged in a couple of minutes. The API checks with ETA before sending it again, so it won't be duplicated.
ETA_REJECTION ETA rejected this receipt because its data didn't pass ETA's checks. message holds ETA's reason and the field it refers to. Correct the receipt, then resend it with the same receipt number.
ETA_SUBMISSION_ERROR Returned in errorCode. ETA refused the whole request, so none of the receipts sent in it were registered. Typical causes: a malformed receipt, ETA credentials or permissions, a request that is too large, too many requests, or ETA being temporarily unavailable. field holds ETA's HTTP status (for example TooManyRequests); details holds ETA's message and, when ETA gives one, how long to wait. If ETA asked you to wait, keep the receipts queued and resend them after that time. Otherwise fix the cause given in details, then resend.
ETA_DUPLICATE_PAYLOAD Returned in errorCode. ETA received an identical request in the last 10 minutes, refused this one as a duplicate, and hasn't confirmed the receipts yet. Each affected receipt has a RECEIPT_OUTCOME_UNKNOWN entry in errors. Keep the receipts queued and resend them in a few minutes; details gives the wait time when ETA provides one. Once ETA confirms them, you get the original result with alreadySubmitted: true.
VALIDATION_ERROR The request has data the API can't use. The same receipt number twice in one request refuses the whole request (errorCode is set and nothing is sent). A discount that can't be applied, or a receipt number, device serial number or header.previousUUID that is too long, refuses only that receipt. details says what to correct. Correct the data and resend.
ETA_AUTH_ERROR Returned in errorCode. The API couldn't sign in to ETA with the customer's ETA credentials, so no receipts were sent. details holds the reason. Check the customer's ETA client ID and client secret, update them with PUT /api/Customers/{id} if needed, then resend the receipts.
BUSINESS_LOGIC_ERROR Returned in errorCode. Sending these receipts would go over your account's annual receipt limit, so none of the new receipts were sent. message shows the limit and how many receipts are used. Contact support to raise the limit, then resend the receipts.
SERVER_ERROR Returned in errorCode. The problem is on our side: the API couldn't record the receipts before sending them, so none of them were sent to ETA. Resend them in a minute; nothing was duplicated. If it keeps happening, contact support.

Support and Tracing

Every response carries an X-Trace-Identifier header. Submit responses (POST /api/Receipts/submit and POST /api/Invoices/submit) also return a requestId in the body. Log one of them with each call, and quote it when you contact support, so we can find that exact request straight away.

To use your own reference instead, send an X-Correlation-Id header (up to 100 characters), for example your queue entry ID or POS transaction ID. It's returned as requestId, and we can find the request by that value too.

HTTP Header
X-Correlation-Id: POS-0042-Q-000123

Ready to explore interactively?

Use the Scalar API explorer to test every endpoint with live requests.

Open API Explorer