Skip to content

Submit a document

POST
/api/integration/v1/outbound

Submits one UBL 2.1 document for delivery.

By default the response does not wait for processing: status 210 means “received by us”.

Send Prefer: wait=N (seconds, at most 30) to wait for the outcome of our processing up to the submission to UJP. The answer then carries the status reached and, once UJP accepted the document, its identifier in ujp.euid. If the time runs out first, the answer carries status 210 or 220 and ujp is null — follow the document with GET outbound/{docUid}. Later changes on UJP’s side (the recipient accepting or refusing) are not waited for. The answer names the wait that was applied in the Preference-Applied header.

Repeating the call with the same X-Idempotency-Key and the same body within one hour returns the same docUid with HTTP 200 instead of creating a second document.

The example applies to the demo environment only.

Request

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: <X-Idempotency-Key>" \
  -H "Prefer: <Prefer>" \
  -H "Content-Type: application/xml" \
  --data-binary @invoice.xml

Every call carries X-Api-Key with a key that has the IntegrationApi scope, and X-Software-Id with the name and version of your software. How to request a key is on Authentication.

Parameters

All parameters (3)
ParameterInTypePresenceDescription
X-Software-IdheaderstringalwaysIdentifier of the calling software, e.g. SSBFocus/4.2.1. Free text, recorded in the audit trail. Required on every call; missing means EARH-1008.
X-Idempotency-KeyheaderstringalwaysClient-chosen key that makes the submission repeatable. The same key with the same body within one hour returns the same docUid instead of creating a second document; the same key with a different body is rejected with EARH-2010. Missing means EARH-2013.
PreferheaderstringoptionalOptional. wait=N makes the answer wait up to N seconds (at most 30) for the outcome of our processing up to the submission to UJP, and return the status reached together with ujp.euid. Omit it and the answer returns at once with status 210.

Request Body

One UBL 2.1 document as raw XML. Exactly one document per request — a payload carrying more is rejected with EARH-2011. The bytes are stored as received and are returned unchanged by GET outbound/{docUid}/file?format=ubl.

Type: application/xml. A working example that passes as it is: invoice-ubl21-en.xml — what it contains and what you may replace is on Formats.

Error codes

EARH-1001 · EARH-1002 · EARH-1003 · EARH-1004 · EARH-1005 · EARH-1006 · EARH-1007 · EARH-1010 · EARH-1011 · EARH-2001 · EARH-2002 · EARH-2003 · EARH-2004 · EARH-2005 · EARH-2006 · EARH-2007 · EARH-2010 · EARH-2011 · EARH-2012 · EARH-2013 · EARH-9001

The full list of codes, what each means and what to do, is in the Error catalogue.

The same key and body were already submitted; the original docUid is returned and alreadyExisted is true.

{
  "docUid": "eah_o_01M0ZZB88FT117S27PM8ZEK9RY",
  "status": 210,
  "receivedAt": "2026-08-26T23:24:26+02:00",
  "alreadyExisted": true
}
All fields in the body (6)
FieldTypePresenceDescription
docUidstringalwaysIdentifier of the document. Every later call about this document uses it.
statusinteger · int32alwaysStatus of the document at the moment of the answer. Without Prefer: wait it is 210 (Received). With it, it is the status our processing reached — 210 or 220 when the time ran out first; then follow the document with GET outbound/{docUid}.
receivedAtstring · date-timealwaysTime the document was taken over.
alreadyExistedbooleanalwaystrue when the same idempotency key and the same body were already submitted, so this call returned the original document instead of creating a second one. Then the answer carries HTTP 200; a newly created document carries 201.
failureCodestringnullableThe EARH-4xxx code when processing has already failed — the same field as on the document detail; null otherwise.
ujp—nullableThe answer received from UJP, with the identifier UJP assigned (euid). null while the document has not been forwarded to UJP — always the case without Prefer: wait.

The document was accepted and now carries a docUid.

{
  "docUid": "eah_o_01M0ZZB88FT117S27PM8ZEK9RY",
  "status": 210,
  "receivedAt": "2026-08-26T23:24:17+02:00",
  "alreadyExisted": false
}
All fields in the body (6)
FieldTypePresenceDescription
docUidstringalwaysIdentifier of the document. Every later call about this document uses it.
statusinteger · int32alwaysStatus of the document at the moment of the answer. Without Prefer: wait it is 210 (Received). With it, it is the status our processing reached — 210 or 220 when the time ran out first; then follow the document with GET outbound/{docUid}.
receivedAtstring · date-timealwaysTime the document was taken over.
alreadyExistedbooleanalwaystrue when the same idempotency key and the same body were already submitted, so this call returned the original document instead of creating a second one. Then the answer carries HTTP 200; a newly created document carries 201.
failureCodestringnullableThe EARH-4xxx code when processing has already failed — the same field as on the document detail; null otherwise.
ujp—nullableThe answer received from UJP, with the identifier UJP assigned (euid). null while the document has not been forwarded to UJP — always the case without Prefer: wait.

The submission cannot be processed — EARH-1007 (a tenant value in the body), EARH-2001 (not well-formed XML), EARH-2002 (not UBL 2.1), EARH-2003 (no recipient), EARH-2004 (malformed recipient tax number), EARH-2005 (no document type), EARH-2006 (no amount), EARH-2007 (the issuer is not the organization this key belongs to), EARH-2011 (more than one document), EARH-2013 (the X-Idempotency-Key header is missing).

{
  "type": "https://earhiva.mk/errors/EARH-1007",
  "title": "Request body must not contain a tenant-identifying field.",
  "status": 400,
  "code": "EARH-1007"
}
All fields in the body (6)
FieldTypePresenceDescription
typestringalwaysAddress of this code in the error catalogue (RFC 7807).
titlestringalwaysShort, stable English summary of the error — the same string that appears in your log.
statusinteger · int32alwaysHTTP status code, repeated in the body (RFC 7807).
detailstringnullableWhat went wrong in this particular call, when there is more to say than the title. Absent when the title already says everything.
codestringalwaysThe EARH-xxxx code. This is the contract: match on it, not on the text.
eventIdstringnullableOnly on EARH-9000. Quote it when reporting the problem — it is the same reference we record in our log, so one number identifies the event on both sides.

The API key is missing, unknown, expired or revoked — EARH-1001, EARH-1002, EARH-1003. The body is the standard error body — the same on every call. What to do about each code is in the Error catalogue.

The caller’s address is outside the allowed list, the key does not carry the IntegrationApi scope, the organization’s plan does not include integration access, or the key has been switched off by its owner — EARH-1004, EARH-1005, EARH-1010, EARH-1011. The last two are returned on every call and carry no Retry-After: waiting does not change the state. The body is the standard error body — the same on every call. What to do about each code is in the Error catalogue.

EARH-2010 — the same idempotency key was already used with different content; EARH-9001 — outbound over the API is not enabled for this tenant.

{
  "type": "https://earhiva.mk/errors/EARH-2010",
  "title": "This idempotency key was already used with different content.",
  "status": 409,
  "code": "EARH-2010"
}
All fields in the body (6)
FieldTypePresenceDescription
typestringalwaysAddress of this code in the error catalogue (RFC 7807).
titlestringalwaysShort, stable English summary of the error — the same string that appears in your log.
statusinteger · int32alwaysHTTP status code, repeated in the body (RFC 7807).
detailstringnullableWhat went wrong in this particular call, when there is more to say than the title. Absent when the title already says everything.
codestringalwaysThe EARH-xxxx code. This is the contract: match on it, not on the text.
eventIdstringnullableOnly on EARH-9000. Quote it when reporting the problem — it is the same reference we record in our log, so one number identifies the event on both sides.

EARH-2012 — the document exceeds the working size limit; the response names the allowed one.

{
  "type": "https://earhiva.mk/errors/EARH-2012",
  "title": "Document exceeds the configured size limit.",
  "status": 413,
  "code": "EARH-2012"
}
All fields in the body (6)
FieldTypePresenceDescription
typestringalwaysAddress of this code in the error catalogue (RFC 7807).
titlestringalwaysShort, stable English summary of the error — the same string that appears in your log.
statusinteger · int32alwaysHTTP status code, repeated in the body (RFC 7807).
detailstringnullableWhat went wrong in this particular call, when there is more to say than the title. Absent when the title already says everything.
codestringalwaysThe EARH-xxxx code. This is the contract: match on it, not on the text.
eventIdstringnullableOnly on EARH-9000. Quote it when reporting the problem — it is the same reference we record in our log, so one number identifies the event on both sides.

The traffic limit for this key has been reached — EARH-1006. The response carries Retry-After. The limit is counted per key, and its size comes from the organization’s plan. The body is the standard error body — the same on every call. What to do about each code is in the Error catalogue.

Unexpected error on our side — EARH-9000. The body carries eventId; quote it when reporting the problem. The body is the standard error body — the same on every call. What to do about each code is in the Error catalogue.