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.
The path of a received invoice
Section titled “The path of a received invoice”A received invoice travels the opposite way from a submitted one, and you are the one who decides:
| # | Where | What happens |
|---|---|---|
| 1 | The sender | issues the invoice and sends it through the tax authority |
| 2 | The tax authority | records it and hands it to us |
| 3 | eArhiva.mk | takes it, keeps it and offers it to you over the API |
| 4 | Your software | fetches 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.
What you need
Section titled “What you need”| What | Why |
|---|---|
| a sandbox key | the same one as for outbound; if you already have it, go to step 2 |
the curl command | for the calls; it ships with Windows 10 and 11, macOS and Linux |
jq or any JSON viewer | the answers here are JSON, not XML |
The steps
Section titled “The steps”-
Ask for access
Section titled “Ask for access”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. -
Check that the key works
Section titled “Check that the key works”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 OKand the body{"ok": true, "keyValid": true, …}.You get What it means What you do 401withEARH-1001the key is not copied exactly, or it has expired copy it again, with no spaces around it 400withEARH-1008the X-Software-Idheader is missingadd it — it is required on every call -
Create test documents
Section titled “Create test documents”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]}📌
seedis 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 means1.📌 Read
expect. For every document it says exactly what should happen with it — write your assertion from there instead of guessing.You get What it means What you do 400withEARH-2030a scenario name that does not exist the answer lists the allowed ones — take it from there 404withEARH-1009you called it outside the sandbox this 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. -
Fetch the new invoices
Section titled “Fetch the new invoices”onlyNotImported=truegives 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 want Add only one sender &senderEdb=MK4080012345678only from a date &dateFrom=2026-09-01T00:00:00Za bigger page &pageSize=500— that is also the highest allowedYou get What it means What you do 400withEARH-3005you asked for a page larger than 500 the answer names the allowed size 400withEARH-2020a parameter cannot be read detailnames it — a date in the wrong shape, for instance -
Read the detail
Section titled “Read the detail”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}Field What your software does with it status110received,120being processed,140approved,150rejecteddecisionDeadlinethe 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; nullmeans you have not yetAn unknown
docUidreturns404withEARH-3001— the same answer as for a document belonging to another organisation, deliberately. -
Fetch the content
Section titled “Fetch the content”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.binformatWhat 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-Dispositionwith a name derived from thedocUid.You get What it means What you do 404withEARH-3002that representation does not exist for this document try original; if that returns3002too, the document carries no content400withEARH-2020formatis missingadd it — there is no default 409withEARH-9003the representation existed and its content is not retrievable write to support with the docUid; everything else about the document still reads normally -
Acknowledge that you fetched it
Section titled “Acknowledge that you fetched it”The acknowledgement says the document has entered your system. After it, the document leaves the
onlyNotImported=truelist: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.
-
Decide — accept or reject
Section titled “Decide — accept or reject”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
nameof every reason arrives in Macedonian, as the tax authority publishes it — we do not translate it. Branch oncode; shownameto 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}📌
200means “recorded with us”, not “confirmed by the tax authority”. Reporting onward follows; whether they have confirmed is inconfirmedByTaxAuthority. Read the state withGET inbound/{docUid}: after an acceptance the status becomes140, after a rejection150andrejectionReasoncarries the reason text.If it is not:
You get What it means What you do 400withEARH-2022the body is unusable: a rejection without a reason or without a comment, an acceptance carrying either, or an inactive reason detailsays which of the four; fix the body409withEARH-3006too late: the deadline has passed, or the decision is already confirmed nothing to fix — the state does not come back 404withEARH-3001no such document check the docUid409withEARH-9002the decision cannot be handed on because of something on our side nothing 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. Theacceptedfield is required. -
Pull only what changed
Section titled “Pull only what changed”For everyday work do not pull the whole list. With
by=updatedthe answer carriesnextFrom— 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=createddoes not returnnextFrom— there it isnull. Incremental pulling is promised only withby=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.
Where to go next
Section titled “Where to go next”| When you need | Go to |
|---|---|
| the statuses of a received document and the transitions between them | Inbound documents |
| a series of documents by scenario, each one explained | A series by scenario |
| one invoice with your own content and your own PDF | One invoice with your own content |
the meaning of every EARH-xxxx code | Error catalogue |
| sending invoices instead of receiving them | Process: outbound e-invoices |