Connector (send in one call)
Single-call sending via POST /v1/agent/peppol/connector/send. One call does everything: it validates the supplied UBL/CII XML (+ optional auto-repair via normalization), reserves credit, and queues the document for sending via Peppol. A replacement for chaining preflight → send, with three safety guards.
X-API-Key and X-Organization-Id headers, scope invoice:send. A test key (efk_pk_test_…) routes to the sandbox AP and never charges credit; a live key sends for real and consumes credit.Endpoint
| Method | Path | Scope | Headers |
|---|---|---|---|
| POST | /v1/agent/peppol/connector/send | invoice:send | X-API-Key, X-Organization-Id, Idempotency-Key |
Safety guards
Three guards make a single call safe even on retries or partial failure:
| Guard | Behavior |
|---|---|
| Idempotency-Key is required | Omitting the header returns 400. The same key with a different request body returns 409. A replay (same key and body) returns the stored response and never sends twice. |
| Credit only when ready | Credit is reserved only when the document is send_ready: true. A document with errors is rejected before reservation. Credit is never over-consumed and the charge happens exactly once, at send time (no double billing). |
| Auto-repair is opt-in | Normalization changes are applied only when options.autoRepair: true, and each one is listed in repair_applied: a modified document is never sent silently. Without autoRepair, a document that needs changes is rejected as needs_repair. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
receiverPeppolId | string | No | Peppol identifier of the recipient (e.g. 0245:DIČ). When provided, an SMP lookup also runs and fills in the recipient field. |
document | object | Yes | Source document: see below. |
document.format | ubl | cii | Yes | Format of the source XML. |
document.xmlBase64 | string (base64) | Yes | Base64-encoded UBL / CII XML. |
options.autoRepair | boolean | No | When true, normalization changes (BIS 3.0 identifiers, CII→UBL) are applied and the document is sent. Defaults to false. |
options.validateOnly | boolean | No | When true, performs only validation (status validated), and doesn't reserve credit or send. |
options.dispatch | now | later | No | Defaults to now (reserves credit and queues immediately). later only stages the document without charging credit and returns staged_id: see "Prepare now, send later" below. |
POST /v1/agent/peppol/connector/send
X-API-Key: efk_pk_test_...
X-Organization-Id: 0a2c1f3e-…
Idempotency-Key: 7d1f2c9a-3b4e-4f5a-8c6d-1e2f3a4b5c6d
Content-Type: application/json
{
"receiverPeppolId": "0245:2123456789",
"document": {
"format": "ubl",
"xmlBase64": "PD94bWwgdmVyc2lvbj0iMS4wIj8+..."
},
"options": { "autoRepair": true }
}Response
The response is 200 whenever the operation ran. The actual result is in the status field (queued / rejected / validated) and send_ready. The invoice_id, document_id, and job_id fields are populated only when status: queued.
{
"data": {
"status": "queued",
"invoice_id": "b1f0d7a2-…",
"document_id": "0245:2123456789#OF2026001",
"job_id": "0245:2123456789#OF2026001",
"send_ready": true,
"validator_unavailable": false,
"validation": {
"ran": true,
"valid": true,
"error_count": 0,
"warning_count": 1
},
"repair": [],
"repair_applied": [
{ "change": "Added Peppol BIS 3.0 identifiers (CustomizationID/ProfileID)." }
],
"recipient": {
"peppol_id": "0245:2123456789",
"found": true,
"sml_state": "active"
}
}
}| Field | Type | Description |
|---|---|---|
status | queued | rejected | validated | queued = credit reserved + queued for sending; rejected = not sent (see reason); validated = validation only (validateOnly). |
reason | string | null | Rejection reason (only for rejected): validation | needs_repair | credit | validator_unavailable. |
invoice_id | string | null | ID of the created invoice (only for queued). |
document_id | string | null | Document identifier in the transmission (only for queued). |
job_id | string | null | ID of the queued sending job (only for queued). |
send_ready | boolean | null | true = the document was ready to send. null when the validator wasn't available. |
validator_unavailable | boolean | true if the validation runtime wasn't available, and send_ready is then null. |
validation | object | Run summary: ran, valid, error_count, warning_count. |
repair | array of objects | Findings / suggested fixes: field, code, message, severity (error | warning). |
repair_applied | array of objects | Specific changes applied by auto-repair: field, change. Populated only when autoRepair=true and the document was sent. |
recipient | object | null | Result of the recipient SMP lookup, if receiverPeppolId was provided; otherwise null. |
Prepare now, send later
When you want to first prepare (validate) a document and send it only later (for example after approval), send options.dispatch: "later". A staged document doesn't reserve or charge credit; the response has status: staged and returns staged_id, which you use to send the document in a separate call. The default dispatch: "now" behaves as before (reserves credit and queues immediately).
POST /v1/agent/peppol/connector/send
{
"document": { "format": "ubl", "xmlBase64": "PD94bWwg..." },
"options": { "dispatch": "later" }
}
→ 200
{
"data": {
"status": "staged",
"staged_id": "5f2a9c1b-…",
"send_ready": true,
"validation": { "ran": true, "valid": true, "error_count": 0, "warning_count": 0 }
}
}You send a staged document via POST /v1/agent/peppol/connector/dispatch/{stagedId} (scope invoice:send). Only now is credit checked and reserved, and the document is queued for sending. The call is naturally idempotent: the staged document is deleted on first send, so retrying with the same stagedId returns 404. Staged documents are held temporarily (7 days) and are scoped to the organization.
| Method | Path | Scope | Headers |
|---|---|---|---|
| POST | /v1/agent/peppol/connector/dispatch/{stagedId} | invoice:send | X-API-Key, X-Organization-Id |
POST /v1/agent/peppol/connector/dispatch/5f2a9c1b-…
→ 200
{
"data": {
"status": "queued",
"invoice_id": "b1f0d7a2-…",
"document_id": "0245:2123456789#OF2026001",
"job_id": "0245:2123456789#OF2026001"
}
}| Field | Type | Description |
|---|---|---|
status | queued | rejected | queued = credit reserved + queued for sending; rejected = not sent (see reason). |
reason | string | null | Rejection reason (only for rejected). Currently credit = insufficient credit. |
invoice_id | string | null | ID of the created invoice (only for queued). |
document_id | string | null | Document identifier in the transmission (only for queued). |
job_id | string | null | ID of the queued sending job (only for queued). |
404 both when stagedIddoesn't exist and when it has already been sent once: a staged document is deleted after sending, so dispatch never sends twice.Errors
Validation findings about the document itself are not a 4xx error; they're always part of the 200 response, in the status / repair fields. 4xx status codes are only for protocol errors:
| Code | Meaning |
|---|---|
400 | The Idempotency-Key header is missing, or the body / document is invalid. |
401 | Invalid or missing API key. |
403 | Missing invoice:send scope or invalid organization. |
409 | The Idempotency-Key was already used with a different request body. |
Idempotency-Key is required. Use a stable key derived from the invoice number (not a random one on every retry): otherwise a retry after a timeout could create a second send. A replay with the same key and body is safe and returns the original result.Related
For bulk sending from a table, use CSV ingest. You can look up the recipient separately via Recipient lookup and get delivery evidence after sending via Delivery evidence.