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.
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.
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.
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.
/api/Auth/login.Authorization: Bearer <token>
X-Api-Key: <customer-api-key>
Obtaining a JWT Token (Partner API)
POST /api/Auth/login
Content-Type: application/json
{
"email": "partner@yourcompany.com",
"password": "your-password"
}
{
"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.
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.
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.
Create a new partner account. Returns a JWT token on success.
Authenticate with email and password. Returns a JWT Bearer token and its expiration date.
{
"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.
List all customers registered under your partner account, including their active API key and signature client connection status.
Retrieve a single customer by ID.
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.
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.
Permanently remove a customer ERP and all associated data from your account.
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.
Generate a new API key for the customer, revoking any previously active keys. Returns the updated customer info including the new key.
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.
- You register the customer and set their ERP Callback URL (the endpoint in their own system).
- You register
https://eta-api.avtax.net/{customerApiKey}as the callback base URL inside ETA portal for that customer. - ETA calls
PUT /{customerApiKey}/notifications/documentsorPUT /{customerApiKey}/notifications/receiptson our server. - We validate the key, translate the payload, and
POSTa simplified notification to the customer's ERP Callback URL. - The customer's ERP returns
200 OKand processes the update.
ETA calls this when a document event occurs (validated, received, rejected, cancelled). We forward a simplified POST to the customer's ERP Callback URL.
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.
{
"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"
}
]
}
"document" or "receipt"document-validated, Validated, Cancelled)Valid, Invalid, Cancelled, Rejected{
"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
Submit one or more invoices. Supports: I (Invoice),
C (Credit Note), D (Debit Note).
Request cancellation of a previously submitted document. Provide the document uuid and a cancellation reason.
Reject a document received from a supplier. Provide the document uuid and a rejection reason.
Decline a rejection request received for one of your submitted documents. Provide the document uuid.
Search submitted or received documents using filters: date range, direction, status, type, receiver/issuer ID, internal ID, UUID, and pagination.
Retrieve full validation details and the complete document JSON for a specific invoice.
Download the ETA-generated PDF for a specific invoice. Returns a binary PDF file.
Retrieve the processing status and individual document results for a specific batch submission UUID.
Document Types
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.
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.
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.
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.
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.
Same as /details, plus rawDocument: the receipt document exactly as ETA received and stored it,
as a JSON string.
Retrieve full processing details and individual receipt statuses for a specific batch submission UUID.
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.
header.receiptNumberandheader.dateTimeIssued(UTC)seller, includingseller.deviceSerialNumber- Each line:
quantity,unitPricebefore tax, and its tax codes and rates intaxableItems - Discounts, each as an amount or a rate
- Optional: the totals printed on the receipt, in
expectedTotals
- The amounts on each line, including its tax
totalSales,netAmount,taxTotalsandtotalAmount- The discount totals
- The receipt UUID and
header.previousUUID - All of it comes back in
totalswhen 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.
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.
{
"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.
{
"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
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.
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.
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.
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.
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.
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.
GET /api/Receipts/status?deviceSerialNumber=POS-0001&receiptNumber=T12-1001 X-Api-Key: <customer-api-key>
{
"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
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.
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.
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.
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"
}
]
}
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.
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