Process: outbound e-invoices
This page takes you from nothing to one outbound e-invoice that the tax authority (UJP) has received and accepted. You do not need to know anything in advance: every step says what you do, gives you a ready-made 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). It sends the
invoice to UJP’s test system, so no invoice on this page is real and none creates a tax
liability.
The path of the invoice
Section titled “The path of the invoice”Before the steps, the whole picture. The invoice passes through four places:
| # | Where | What happens |
|---|---|---|
| 1 | Your software | builds the invoice as UBL 2.1 XML and sends it with one call |
| 2 | eArhiva.mk | registers it, checks it, turns it into a message for UJP and signs it |
| 3 | UJP | receives it, checks it, and accepts or refuses it |
| 4 | The recipient | sees it on their side and may accept or refuse it |
You send only once (step 4 below), and after that you only ask how far it got (steps 5 and 6). Signing and sending to UJP are done by us.
What you need
Section titled “What you need”| What | Why |
|---|---|
| for the access request | |
the curl command | for the calls; it ships with Windows 10 and 11, macOS and Linux |
| one empty folder | you save the invoice there and run the commands from it |
| details of the client | on whose behalf you send — name, tax number and e-UJP ID (see step 1) |
The steps
Section titled “The steps”-
Request access
Section titled “Request access”Access is requested from us by e-mail, at support@earhiva.mk, with the subject
Request for access to the sandbox server. Copy the text below and fill in all three parts:Text of the request · copy it and fill it in Request for access to the eArhiva.mk sandbox server1. Integrator detailscompany name:contact person:e-mail:phone:name of the application that will use the API:2. Client details (on whose behalf the invoices will be sent)company name:tax number (EDB):e-UJP ID:contact person:e-mail:3. Recipient of the test invoicescompany name:tax number (EDB):All three parts are needed because the invoice reaches UJP only if three things are prepared in advance:
What Who provides it What happens without it the client’s tax number the client the invoice does not leave for UJP the client’s e-UJP ID issued by UJP when the client registers for e-invoicing; the client has it the invoice does not leave for UJP the recipient registered as the client’s partner ⚠️ no longer a prerequisite — a recipient that is missing is registered from the invoice itself; part 3 still helps us check acceptance before you do — What you get back: a key (a long string of characters) and a confirmation that the client’s organization is ready for sending to UJP. Keep the key like a password — it sends on the client’s behalf.
-
Check that the key works
Section titled “Check that the key works”This is the shortest call: it sends nothing, it only tells you whether the key is accepted. In the command, replace the two values in
< >:<YOUR KEY>— the key from step 1<NAME AND VERSION OF YOUR APPLICATION>— free text, for exampleSmetkovodstvoPro/2.4
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>"If all is well, the first line of the answer is
HTTP/1.1 200 OK, and at the end it says:Answer from the API · the values are an example {"ok": true,"keyValid": true,"serverTime": "2026-09-11T10:14:03.412+02:00"}If it is not:
You get What it means What you do 401the key was not copied exactly, or it has expired copy it again, with no spaces at the start or the end 403the key does not carry the right to the Integration API, or the address you send from is not allowed write to support and send the whole answer 400withEARH-1008the X-Software-Idheader is missingadd it — it is required on every call -
Prepare the invoice
Section titled “Prepare the invoice”Download the ready-made invoice
invoice-ubl21-en.xmland save it in the empty folder under the nameinvoice.xml.Open it in any text editor and change exactly four things. Leave everything else as it is.
# What Where in the file With what 1 invoice number <cbc:ID>0001/2026</cbc:ID>a new number, for example TEST-00012 issue date <cbc:IssueDate>…</cbc:IssueDate>today’s date, in the form 2026-09-113 sender cac:AccountingSupplierParty—cbc:Nameandcbc:CompanyIDthe name and tax number of the client from part 2 of the request 4 recipient cac:AccountingCustomerParty—cbc:Name,cbc:CompanyIDand the address incac:PostalAddressthe name, tax number and address of the recipient from part 3 of the request After the change, those parts look like this:
invoice.xml · only the changed parts <cbc:ID>TEST-0001</cbc:ID><cbc:IssueDate>2026-09-11</cbc:IssueDate><cac:AccountingSupplierParty><cac:Party><cac:PartyName><cbc:Name><CLIENT NAME></cbc:Name></cac:PartyName><cac:PartyTaxScheme><cbc:CompanyID><CLIENT TAX NUMBER></cbc:CompanyID></cac:PartyTaxScheme></cac:Party></cac:AccountingSupplierParty><cac:AccountingCustomerParty><cac:Party><cac:PartyName><cbc:Name><RECIPIENT NAME></cbc:Name></cac:PartyName><cac:PostalAddress><cbc:StreetName><STREET AND NUMBER></cbc:StreetName><cbc:CityName><CITY></cbc:CityName><cbc:PostalZone><POSTAL CODE></cbc:PostalZone><cac:Country><cbc:IdentificationCode>MK</cbc:IdentificationCode></cac:Country></cac:PostalAddress><cac:PartyTaxScheme><cbc:CompanyID><RECIPIENT TAX NUMBER></cbc:CompanyID></cac:PartyTaxScheme></cac:Party></cac:AccountingCustomerParty>⛔ Take a new invoice number and a new
X-Idempotency-Keyon every attempt. The same key with the same content returns the first answer and creates no second document (see step 4). But the same invoice sent with a new key creates a second document here and goes to UJP a second time — and whether they accept it or refuse it as a duplicate is up to their register, not something we control. So:TEST-0002,TEST-0003and so on.⛔ Do not remove the recipient’s address or the line’s VAT rate (
cac:ClassifiedTaxCategory). Acceptance does not check them, but UJP requires them: without them the invoice stops before UJP withEARH-4001. -
Send the invoice
Section titled “Send the invoice”Run the command from the folder where
invoice.xmlis. Besides the two headers you already know, this request carries two more:-
X-Idempotency-Key— your label for this sending. Take it from the invoice number, for exampleinvoice-TEST-0001. If the connection drops and you repeat the same command, no second invoice is created — you get the answer from the first time.⛔ The label is bound to the content, not only to itself. The same key with the same content is a repeat and returns
200; the same key with changed content is refused withEARH-2010. So if you regenerate the document after a fix, it needs a new label. Because of that, a label derived from the invoice identity alone (company + year + type + number) runs into409on every second attempt — add something that changes with the content (a revision, the generation time, a digest of the body). -
Prefer: wait=30— makes the answer wait up to 30 seconds while our processing submits the invoice to UJP, and return the outcome together with the number UJP assigned to it. Without this header the answer comes at once, only confirming that the invoice was received.⚠️
waitis an upper bound on the waiting, not a guarantee of a final outcome. If the processing takes longer, the answer comes when the time is up carrying the state that holds at that moment — most often210. That is not an error and does not mean something failed: the state is read on withGET outbound/{docUid}. ThePreference-Appliedheader says how long was actually waited.
Request · this is what you run curl -i -X POST "https://sandbox.api.earhiva.mk/api/integration/v1/outbound" \-H "X-Api-Key: <YOUR KEY>" \-H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>" \-H "X-Idempotency-Key: invoice-TEST-0001" \-H "Prefer: wait=30" \-H "Content-Type: application/xml" \--data-binary @invoice.xmlIf all is well, the first line is
HTTP/1.1 201 Created, the headers includePreference-Applied: wait=30, and at the end it says:Answer from the API · the values are an example, yours will differ {"docUid": "eah_o_01K3F9YB2M7Q4XVZ8NDHRPTC5E","status": 270,"receivedAt": "2026-09-11T10:16:41.088+02:00","alreadyExisted": false,"failureCode": null,"ujp": {"euid": "<UJP ID>", // the number UJP assigned to the invoice"qrLink": "https://…/euid/<UJP ID>", // the QR code link, exactly as UJP returned it"state": null,"code": null,"message": null,"checkedAt": null}}📌
euidandqrLinkare filled and the other three are empty — that is what it should look like. When UJP accepts an invoice it returns an identifier and a link, not a state. Thestate,code,messageandcheckedAtfields are filled later, when the state is read back from UJP — ask withGET outbound/{docUid}when you need it. ⛔ Do not wait for them to conclude the invoice was accepted:euidis what says that.📌 Copy the value of
docUidand keep it. It is the invoice’s number with us, and every following command needs it. The value above is only an example.Where you go next depends on the answer:
In the answer What you do ujpis notnullthe outcome from UJP is already here — ✅ go straight to step 6 statusis210or220, and"ujp": nullthe 30 seconds ran out before the outcome — not an error, processing goes on. Go to step 5 statusis225or250the invoice stopped before UJP or UJP refused it — go to step 6, it says what to do If it is not:
You get What it means What you do 400with a code fromEARH-2001toEARH-2007the file is not right — bad XML, a required field is missing, or the issuer is not your organization ( EARH-2007)the code says which; look it up in the error catalogue, fix it and send again 400withEARH-2011the body carries more than one invoice one request carries exactly one document; split them 400withEARH-2013the X-Idempotency-Keyheader is missingadd it — without it we cannot tell a retry apart from a new invoice 409withEARH-2010the same X-Idempotency-Keywas already used for different contentchange the invoice number and the label — for example TEST-0002andinvoice-TEST-0002413withEARH-2012the invoice is too large the response names the size allowed for that organisation; drop the attachment that pushes it over 200instead of201, with"alreadyExisted": truethis invoice was already sent with the same label not an error — the docUidis the one from the first time -
-
Follow it until it reaches UJP
Section titled “Follow it until it reaches UJP”You need this step only if the answer from step 4 carried no outcome from UJP. Processing goes on in the background — the invoice is checked, signed and sent to UJP. With the command below you ask how far it got, using the eArhiva ID. Replace
<docUid>with the value from step 4:Request · this is what you run curl -i "https://sandbox.api.earhiva.mk/api/integration/v1/outbound/<docUid>" \-H "X-Api-Key: <YOUR KEY>" \-H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>"Watch the
statusfield in the answer. Repeat the command every 10 seconds until the status becomes one of the last two rows of the table:statusWhat it means What you do 210received by us, processing has not started yet wait and ask again 220being checked and signed wait and ask again 230submitted to UJP ✅ go to step 6 270archived — after submission the invoice goes to the archive by itself ✅ go to step 6 225or250it stopped before UJP, or UJP refused it go to step 6 — it says what to do Archiving follows immediately after submission, so you may never see
230and get270straight after220. That is not a problem.If the status stays at
210or220for longer than 10 minutes, write to support and send thedocUid. -
Check that UJP accepted it
Section titled “Check that UJP accepted it”In the last answer — from step 4 or from step 5 — check three fields. The invoice is accepted by UJP when all three hold:
Field Must be status230,240or270failureCodenullujpnot null, inside itcodeisnull, andeuidcarries the UJP IDThis is what a successful answer looks like:
Answer from the API · invoice accepted by UJP {"docUid": "eah_o_01K3F9YB2M7Q4XVZ8NDHRPTC5E","status": 270,"failureCode": null, // nothing failed"failureMessage": null,"createdAt": "2026-09-11T10:16:41.088+02:00","partnerEdb": "<RECIPIENT TAX NUMBER>","documentNumber": "TEST-0001","amount": 1180.00,"currency": "MKD","validationErrors": [],"ujp": { // not null — the invoice was at UJP"euid": "<UJP ID>", // the number UJP assigned to the invoice"qrLink": "https://…/euid/<UJP ID>", // the QR code link, exactly as UJP returned it"state": null, // filled later, when the state is read back"code": null, // UJP had no objection"message": null,"checkedAt": null}}If you print the invoice in your own PDF, its QR code is
ujp.qrLink— print that value as it is. ⛔ Do not build it fromeuid: the address differs between test and production, and UJP sets it. WithPrefer: waitfrom step 4,euidandqrLinkarrive in the same answer.The whole path of the invoice, with the time of each change, is shown by the history:
Request · this is what you run curl -i "https://sandbox.api.earhiva.mk/api/integration/v1/outbound/<docUid>/history" \-H "X-Api-Key: <YOUR KEY>" \-H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>"Answer from the API · an invoice that went all the way {"docUid": "eah_o_01K3F9YB2M7Q4XVZ8NDHRPTC5E","transitions": [{ "status": 210, "at": "2026-09-11T10:16:41", "failureCode": null },{ "status": 220, "at": "2026-09-11T10:16:49", "failureCode": null },{ "status": 230, "at": "2026-09-11T10:17:03", "failureCode": null },{ "status": 270, "at": "2026-09-11T10:17:05", "failureCode": null }]}If it was not accepted, find your case:
You see What happened What you do statusis225, or270with"ujp": nullthe invoice was checked but not sent to UJP — the client’s organization lacks the tax number or the e-UJP ID write to support and send the docUid; once they confirm, send a new invoice with a new number250withEARH-4005the recipient could neither be resolved nor registered — a rare case, since an unknown recipient is registered from the document itself validationErrorscarriesEARH-5020with the same reason; check the recipient’s tax number ininvoice.xml, and whether that partner was deleted in the client’s organization250withEARH-4001the invoice did not pass the check before UJP every problem is listed in validationErrors, with its ownEARH-5xxxcode; fix that field ininvoice.xml250withEARH-4002UJP refused the invoice the reason is in ujp.codeandujp.message; the meaning of the code is under Tax authority codes250withEARH-4004processing stopped on a technical failure ⛔ look at failedStepfirst: if it isConvertFormat, the document could not be converted and the cause is in your body; if it is another step, write to support and send thedocUidAn invoice in
250does not continue by itself. After the fix, send a new invoice — steps 3 and 4 with a new invoice number and a newX-Idempotency-Key.
If you sent it by mistake
Section titled “If you sent it by mistake”Cancelling is possible only while the invoice has not reached UJP. Once UJP has accepted it, a record with legal effect exists on their side and no call of ours removes it.
curl -i -X POST "https://sandbox.api.earhiva.mk/api/integration/v1/outbound/<docUid>/cancel" \ -H "X-Api-Key: <YOUR KEY>" \ -H "X-Software-Id: <NAME AND VERSION OF YOUR APPLICATION>"| State of the invoice | Answer | What it means |
|---|---|---|
210 or 220 — received, processing is running | 200 with "status": 260 | processing is stopped; the invoice does not go to UJP |
250 — stopped before UJP | 200 with "status": 260 | the document never left, so cancelling is clean |
230, 240 or 270 — already at UJP | 409 EARH-3003 | too late to cancel; the correction goes through a credit note at UJP |
| already cancelled | 409 EARH-3004 | not a bug in your code — it is already done |
unknown docUid | 404 EARH-3001 | check the identifier |
📌 Cancelling does not delete: the document is still read by its docUid, and the transition to
260 stays in the history. That is deliberate — the trail has to remain.
If step 6 holds, your software sends e-invoices to UJP through eArhiva.mk and receives the UJP ID
of each. The same four calls — ping, POST outbound, GET outbound/{docUid},
GET outbound/{docUid}/history — are all you need for the basic flow. In production only the
address and the key change.
📌 Prefer: wait waits only for the submission to UJP. Later changes on UJP’s side — the
recipient accepting or refusing the invoice — are not waited for; read them with
GET outbound/{docUid}. The UJP ID is also in the list GET outbound, in the field ujpEuid.
Where next
Section titled “Where next”| When you need | Go to |
|---|---|
| what each status means and what it moves to | Outbound documents |
| your own invoices instead of the sample — what you may change and what is required | Formats |
the meaning of every EARH-xxxx code | Error catalogue |
| a list of sent invoices, downloading a file, cancelling | API Reference |
| how many calls per minute you may make | Limits and rules |