Skip to content

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.

Before the steps, the whole picture. The invoice passes through four places:

#WhereWhat happens
1Your softwarebuilds the invoice as UBL 2.1 XML and sends it with one call
2eArhiva.mkregisters it, checks it, turns it into a message for UJP and signs it
3UJPreceives it, checks it, and accepts or refuses it
4The recipientsees 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.

WhatWhy
e-mailfor the access request
the curl commandfor the calls; it ships with Windows 10 and 11, macOS and Linux
one empty folderyou save the invoice there and run the commands from it
details of the clienton whose behalf you send — name, tax number and e-UJP ID (see step 1)

  1. 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 server
    1. Integrator details
    company 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 invoices
    company name:
    tax number (EDB):

    All three parts are needed because the invoice reaches UJP only if three things are prepared in advance:

    WhatWho provides itWhat happens without it
    the client’s tax numberthe clientthe invoice does not leave for UJP
    the client’s e-UJP IDissued by UJP when the client registers for e-invoicing; the client has itthe 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.

  2. 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 example SmetkovodstvoPro/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 getWhat it meansWhat you do
    401the key was not copied exactly, or it has expiredcopy 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 allowedwrite to support and send the whole answer
    400 with EARH-1008the X-Software-Id header is missingadd it — it is required on every call
  3. Download the ready-made invoice invoice-ubl21-en.xml and save it in the empty folder under the name invoice.xml.

    Open it in any text editor and change exactly four things. Leave everything else as it is.

    #WhatWhere in the fileWith what
    1invoice number<cbc:ID>0001/2026</cbc:ID>a new number, for example TEST-0001
    2issue date<cbc:IssueDate>…</cbc:IssueDate>today’s date, in the form 2026-09-11
    3sendercac:AccountingSupplierParty — cbc:Name and cbc:CompanyIDthe name and tax number of the client from part 2 of the request
    4recipientcac:AccountingCustomerParty — cbc:Name, cbc:CompanyID and the address in cac: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-Key on 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-0003 and 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 with EARH-4001.

  4. Run the command from the folder where invoice.xml is. 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 example invoice-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 with EARH-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 into 409 on 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.

      ⚠️ wait is 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 often 210. That is not an error and does not mean something failed: the state is read on with GET outbound/{docUid}. The Preference-Applied header 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.xml

    If all is well, the first line is HTTP/1.1 201 Created, the headers include Preference-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
    }
    }

    📌 euid and qrLink are 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. The state, code, message and checkedAt fields are filled later, when the state is read back from UJP — ask with GET outbound/{docUid} when you need it. ⛔ Do not wait for them to conclude the invoice was accepted: euid is what says that.

    📌 Copy the value of docUid and 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 answerWhat you do
    ujp is not nullthe outcome from UJP is already here — ✅ go straight to step 6
    status is 210 or 220, and "ujp": nullthe 30 seconds ran out before the outcome — not an error, processing goes on. Go to step 5
    status is 225 or 250the invoice stopped before UJP or UJP refused it — go to step 6, it says what to do

    If it is not:

    You getWhat it meansWhat you do
    400 with a code from EARH-2001 to EARH-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
    400 with EARH-2011the body carries more than one invoiceone request carries exactly one document; split them
    400 with EARH-2013the X-Idempotency-Key header is missingadd it — without it we cannot tell a retry apart from a new invoice
    409 with EARH-2010the same X-Idempotency-Key was already used for different contentchange the invoice number and the label — for example TEST-0002 and invoice-TEST-0002
    413 with EARH-2012the invoice is too largethe response names the size allowed for that organisation; drop the attachment that pushes it over
    200 instead of 201, with "alreadyExisted": truethis invoice was already sent with the same labelnot an error — the docUid is the one from the first time
  5. 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 status field in the answer. Repeat the command every 10 seconds until the status becomes one of the last two rows of the table:

    statusWhat it meansWhat you do
    210received by us, processing has not started yetwait and ask again
    220being checked and signedwait 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
    225 or 250it stopped before UJP, or UJP refused itgo to step 6 — it says what to do

    Archiving follows immediately after submission, so you may never see 230 and get 270 straight after 220. That is not a problem.

    If the status stays at 210 or 220 for longer than 10 minutes, write to support and send the docUid.

  6. In the last answer — from step 4 or from step 5 — check three fields. The invoice is accepted by UJP when all three hold:

    FieldMust be
    status230, 240 or 270
    failureCodenull
    ujpnot null, inside it code is null, and euid carries the UJP ID

    This 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 from euid: the address differs between test and production, and UJP sets it. With Prefer: wait from step 4, euid and qrLink arrive 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 seeWhat happenedWhat you do
    status is 225, or 270 with "ujp": nullthe invoice was checked but not sent to UJP — the client’s organization lacks the tax number or the e-UJP IDwrite to support and send the docUid; once they confirm, send a new invoice with a new number
    250 with EARH-4005the recipient could neither be resolved nor registered — a rare case, since an unknown recipient is registered from the document itselfvalidationErrors carries EARH-5020 with the same reason; check the recipient’s tax number in invoice.xml, and whether that partner was deleted in the client’s organization
    250 with EARH-4001the invoice did not pass the check before UJPevery problem is listed in validationErrors, with its own EARH-5xxx code; fix that field in invoice.xml
    250 with EARH-4002UJP refused the invoicethe reason is in ujp.code and ujp.message; the meaning of the code is under Tax authority codes
    250 with EARH-4004processing stopped on a technical failure⛔ look at failedStep first: if it is ConvertFormat, the document could not be converted and the cause is in your body; if it is another step, write to support and send the docUid

    An invoice in 250 does not continue by itself. After the fix, send a new invoice — steps 3 and 4 with a new invoice number and a new X-Idempotency-Key.


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.

Request · this is what you run
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 invoiceAnswerWhat it means
210 or 220 — received, processing is running200 with "status": 260processing is stopped; the invoice does not go to UJP
250 — stopped before UJP200 with "status": 260the document never left, so cancelling is clean
230, 240 or 270 — already at UJP409 EARH-3003too late to cancel; the correction goes through a credit note at UJP
already cancelled409 EARH-3004not a bug in your code — it is already done
unknown docUid404 EARH-3001check 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.

When you needGo to
what each status means and what it moves toOutbound documents
your own invoices instead of the sample — what you may change and what is requiredFormats
the meaning of every EARH-xxxx codeError catalogue
a list of sent invoices, downloading a file, cancellingAPI Reference
how many calls per minute you may makeLimits and rules