Skip to content

Process: inbound e-invoices

This page takes you from nothing to a received e-invoice that is fetched, acknowledged and decided on. You need to know nothing in advance: every step says what you do, gives you a ready command, shows the answer to expect and what to do if you get something else.

Everything happens on the sandbox server (https://sandbox.api.earhiva.mk), where you create the test documents yourself with one command — no waiting for a real invoice from a partner.

A received invoice travels the opposite way from a submitted one, and you are the one who decides:

#WhereWhat happens
1The senderissues the invoice and sends it through the tax authority
2The tax authorityrecords it and hands it to us
3eArhiva.mktakes it, keeps it and offers it to you over the API
4Your softwarefetches it, acknowledges it as imported and decides — accepts or rejects

⛔ The decision has a deadline. Every invoice carries decisionDeadline; after that day the tax authority accepts it on its own and a decision sent afterwards is refused. It is the only part of this flow that cannot be repeated.

WhatWhy
a sandbox keythe same one as for outbound; if you already have it, go to step 2
the curl commandfor the calls; it ships with Windows 10 and 11, macOS and Linux
jq or any JSON viewerthe answers here are JSON, not XML

  1. If you already have a key from the outbound process, skip this step — the same key opens both directions.

    If you do not, ask for one by e-mail at support@earhiva.mk, subject Request for access to the sandbox server, naming: your company, a contact person, e-mail, phone and the name of the application that will use the API.

  2. Request · this is what you run
    curl -i "https://sandbox.api.earhiva.mk/api/integration/v1/ping" \
    -H "X-Api-Key: <YOUR KEY>" \
    -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>"

    The first line must be HTTP/1.1 200 OK and the body {"ok": true, "keyValid": true, …}.

    You getWhat it meansWhat you do
    401 with EARH-1001the key is not copied exactly, or it has expiredcopy it again, with no spaces around it
    400 with EARH-1008the X-Software-Id header is missingadd it — it is required on every call
  3. On the sandbox you create the received invoices yourself. One command makes a series of fifteen, one per scenario — with content, without content, with an expired deadline, with a decision already confirmed:

    Request · this is what you run
    curl -i -X POST "https://sandbox.api.earhiva.mk/api/integration/v1/sandbox/seed" \
    -H "X-Api-Key: <YOUR KEY>" \
    -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>" \
    -H "Content-Type: application/json" \
    -d '{"seed": 21, "reset": true}'
    Answer from the API · shortened
    {
    "seed": 21,
    "created": 15,
    "deleted": 15, // reset removed the ones created earlier
    "documents": [
    { "scenario": "full", "docUid": "eah_i_…", "euid": "SEED-21-full",
    "expect": "format=original and format=pdf return 200; format=ubl returns 404" },
    { "scenario": "deadlinePassed", "docUid": "eah_i_…", "euid": "SEED-21-deadlinePassed",
    "expect": "POST decision returns 409 EARH-3006 — the deadline has passed" }
    // … thirteen more
    ]
    }

    📌 seed is a series number, not randomness. The same number gives the same series — same amounts, same dates, same senders. That is why a test written today still holds tomorrow. An omitted number means 1.

    📌 Read expect. For every document it says exactly what should happen with it — write your assertion from there instead of guessing.

    You getWhat it meansWhat you do
    400 with EARH-2030a scenario name that does not existthe answer lists the allowed ones — take it from there
    404 with EARH-1009you called it outside the sandboxthis command exists on the sandbox server only

    If you need one invoice with your own content instead of a series — your own number, amounts and PDF — that is POST sandbox/inbound.

  4. onlyNotImported=true gives you only the ones you have not acknowledged yet — that is the list your software works through:

    Request · this is what you run
    curl -i "https://sandbox.api.earhiva.mk/api/integration/v1/inbound?onlyNotImported=true&pageSize=100" \
    -H "X-Api-Key: <YOUR KEY>" \
    -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>"
    Answer from the API · one row of the list
    {
    "items": [
    {
    "docUid": "eah_i_01M2AA6FNY8D2Y6Z7NXEGZ4810",
    "receivedAt": "2026-09-12T10:02:01.534351",
    "senderName": "Veternik DOOEL",
    "senderEdb": "MK4080012345678",
    "ackedAt": null, // not acknowledged yet
    "status": 120,
    "rejectionReason": null,
    "decisionDeadline": "2026-09-27"
    }
    ],
    "totalCount": 20,
    "page": 1,
    "pageSize": 100,
    "totalPages": 1,
    "nextFrom": null
    }

    Copy the docUid — every following step needs it.

    If you wantAdd
    only one sender&senderEdb=MK4080012345678
    only from a date&dateFrom=2026-09-01T00:00:00Z
    a bigger page&pageSize=500 — that is also the highest allowed
    You getWhat it meansWhat you do
    400 with EARH-3005you asked for a page larger than 500the answer names the allowed size
    400 with EARH-2020a parameter cannot be readdetail names it — a date in the wrong shape, for instance
  5. Request · this is what you run
    curl -i "https://sandbox.api.earhiva.mk/api/integration/v1/inbound/<docUid>" \
    -H "X-Api-Key: <YOUR KEY>" \
    -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>"
    Answer from the API · values are an example
    {
    "docUid": "eah_i_01M2AA6F71MZ3MPCEPTYBADT6H",
    "receivedAt": "2026-09-12T10:02:01.057626",
    "senderName": "Modrina DOO",
    "senderEdb": "MK4030995155258",
    "ackedAt": null,
    "documentNumber": "021-001",
    "amount": 83252.79,
    "currency": null,
    "status": 120,
    "rejectionReason": null,
    "decisionDeadline": "2026-09-27" // ⛔ read this BEFORE you decide
    }
    FieldWhat your software does with it
    status110 received, 120 being processed, 140 approved, 150 rejected
    decisionDeadlinethe day by which the decision must be made; after it the tax authority accepts on its own
    rejectionReasontext, only on a rejected invoice
    ackedAtwhen you fetched it; null means you have not yet

    An unknown docUid returns 404 with EARH-3001 — the same answer as for a document belonging to another organisation, deliberately.

  6. The content is asked for explicitly, with format:

    Request · this is what you run
    curl -i "https://sandbox.api.earhiva.mk/api/integration/v1/inbound/<docUid>/file?format=original" \
    -H "X-Api-Key: <YOUR KEY>" \
    -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>" \
    -o document.bin
    formatWhat it returns
    originalthe file exactly as it arrived — always available when the document carries content at all
    pdfonly if a PDF came with the document
    ublonly if the document arrived as UBL XML

    The answer carries Content-Disposition with a name derived from the docUid.

    You getWhat it meansWhat you do
    404 with EARH-3002that representation does not exist for this documenttry original; if that returns 3002 too, the document carries no content
    400 with EARH-2020format is missingadd it — there is no default
    409 with EARH-9003the representation existed and its content is not retrievablewrite to support with the docUid; everything else about the document still reads normally
  7. The acknowledgement says the document has entered your system. After it, the document leaves the onlyNotImported=true list:

    Request · this is what you run
    curl -i -X POST "https://sandbox.api.earhiva.mk/api/integration/v1/inbound/<docUid>/ack" \
    -H "X-Api-Key: <YOUR KEY>" \
    -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>"
    Answer from the API
    { "docUid": "eah_i_01M2AA6F…", "ackedAt": "2026-09-12T10:02:19.162282" }

    📌 Acknowledging again is not an error and does not move the time — it returns the one from the first call. So call it again freely if you are not sure the first one went through.

    ⛔ An acknowledgement is not a decision. It says “I fetched it”, not “I agree”. The decision is step 8, and only that one reaches the tax authority.

  8. The body carries accepted, and only on a rejection a reason and a comment as well:

    Accepting · this is what you run
    curl -i -X POST "https://sandbox.api.earhiva.mk/api/integration/v1/inbound/<docUid>/decision" \
    -H "X-Api-Key: <YOUR KEY>" \
    -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>" \
    -H "Content-Type: application/json" \
    -d '{"accepted": true}'
    Rejecting · the reason is a code from the code list
    curl -i -X POST "https://sandbox.api.earhiva.mk/api/integration/v1/inbound/<docUid>/decision" \
    -H "X-Api-Key: <YOUR KEY>" \
    -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>" \
    -H "Content-Type: application/json" \
    -d '{"accepted": false, "rejectReasonCode": "O-1", "comment": "Wrong amount on the line"}'

    Read the reason codes from the code list — do not write them from memory:

    Request · this is what you run
    curl -s "https://sandbox.api.earhiva.mk/api/integration/v1/codelists/reject-reasons" \
    -H "X-Api-Key: <YOUR KEY>" \
    -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>"
    Answer from the API · shortened
    {
    "type": "reject-reasons",
    "items": [
    { "code": "O-1", "name": "…" },
    { "code": "O-4", "name": "…" }
    ]
    }

    ⚠️ The name of every reason arrives in Macedonian, as the tax authority publishes it — we do not translate it. Branch on code; show name to the user as it comes.

    If it is right, the answer is 200:

    Answer from the API · the decision is recorded
    {
    "docUid": "eah_i_01M2AA6F…",
    "accepted": true,
    "confirmedByTaxAuthority": false, // the tax authority has not confirmed it yet
    "confirmedAt": null,
    "failureCode": null
    }

    📌 200 means “recorded with us”, not “confirmed by the tax authority”. Reporting onward follows; whether they have confirmed is in confirmedByTaxAuthority. Read the state with GET inbound/{docUid}: after an acceptance the status becomes 140, after a rejection 150 and rejectionReason carries the reason text.

    If it is not:

    You getWhat it meansWhat you do
    400 with EARH-2022the body is unusable: a rejection without a reason or without a comment, an acceptance carrying either, or an inactive reasondetail says which of the four; fix the body
    409 with EARH-3006too late: the deadline has passed, or the decision is already confirmednothing to fix — the state does not come back
    404 with EARH-3001no such documentcheck the docUid
    409 with EARH-9002the decision cannot be handed on because of something on our sidenothing is wrong with your request; retry later, and if it persists write to support

    ⛔ An empty body means a rejection without a reason, not “no decision” — which is why it returns EARH-2022. The accepted field is required.

  9. For everyday work do not pull the whole list. With by=updated the answer carries nextFrom — the starting point for your next call:

    First call
    curl -s "https://sandbox.api.earhiva.mk/api/integration/v1/inbound?by=updated&pageSize=500" \
    -H "X-Api-Key: <YOUR KEY>" -H "X-Software-Id: <YOUR APPLICATION>"
    Next call · with the value from nextFrom
    curl -s "https://sandbox.api.earhiva.mk/api/integration/v1/inbound?by=updated&dateFrom=2026-09-12T09:57:08.4498392%2B02:00&pageSize=500" \
    -H "X-Api-Key: <YOUR KEY>" -H "X-Software-Id: <YOUR APPLICATION>"

    📌 by=created does not return nextFrom — there it is null. Incremental pulling is promised only with by=updated: a document received a month ago whose state changed yesterday belongs to the second, not the first.

    ⚠️ The boundary is inclusive: the last document of the previous call may appear in the next one as well. That is deliberate — better one repeated row than a missed one.


If step 8 holds, your software fetches received e-invoices, downloads their content and decides on them. The five calls — GET inbound, GET inbound/{docUid}, GET inbound/{docUid}/file, POST ack and POST decision — are all you need. In production only the address and the key change, and the documents come from real senders instead of sandbox/seed.

When you needGo to
the statuses of a received document and the transitions between themInbound documents
a series of documents by scenario, each one explainedA series by scenario
one invoice with your own content and your own PDFOne invoice with your own content
the meaning of every EARH-xxxx codeError catalogue
sending invoices instead of receiving themProcess: outbound e-invoices