Skip to content

Invoice Responses

Tell the sender what happened to an invoice you received — accepted, rejected, under query, paid — over the Peppol network.

What an Invoice Response is

An Invoice Response is the Peppol document a buyer sends back to the supplier about an invoice or credit note it received: its status, the reasons for it, and what the supplier is expected to do. getpeppr sends it as a Peppol BIS Invoice Response 3.x (a UBL ApplicationResponse), validated against the Peppol rules before it leaves.

Some e-invoicing rules require receiving software to offer it. You send one with POST /v1/received-documents/{id}/responses, where id is the receivedDocumentId of the inbound.invoice.received or inbound.creditnote.received webhook. Only an invoice or a credit note can be answered: any other received document answers 422 with received_documents.response_document_unsupported.

Response codes

responseCodeMeaningReason required
ABAcknowledged — received, not processed yetNo
IPIn processNo
UQUnder queryYes
CAConditionally acceptedYes
RERejectedYes
APAcceptedNo
PDPaid — use the PPD reason for a partial paymentNo

reasons say why the document has this status (OPStatusReason: NON, REF, LEG, REC, QUA, DEL, PRI, QTY, ITM, PAY, UNR, FIN, PPD, OTH). actions say what the supplier should do (OPStatusAction: NOA, PIN, NIN, CNF, CNP, CNA, OTH). PPD is allowed with PD only; PPD and OTH need a text. At most 10 reasons and actions together; each text up to 500 characters, note up to 1000.

Texts and the note are sent to the supplier and are not stored by getpeppr (only a fingerprint of the request, used for idempotency). The responses you list afterwards carry the codes only — keep your own copy of what you wrote if you need it.

Sending a response

import { Peppol } from "@getpeppr/sdk";

const peppol = new Peppol({ apiKey: "sk_live_..." });

// The id from the inbound.invoice.received webhook (data.receivedDocumentId)
const receivedDocumentId = "3f7c1a52-9d4e-4c1b-8a2f-6b5e0d1c7a93";

const response = await peppol.receivedDocuments.respond(
  receivedDocumentId,
  {
    responseCode: "RE",
    reasons: [{ code: "REF", text: "Purchase order number missing" }],
    actions: [{ code: "NIN" }],
  },
  // Same key on a retry: the response is never sent twice
  { idempotencyKey: `reject-${receivedDocumentId}` },
);

console.log(response.delivery); // "submitted"

The answer is 202 Accepted with delivery: "submitted": the response is on its way, and delivery is asynchronous. Pass an Idempotency-Key: a retry with the same key and body never sends a second response. Once a response has left, the same key with a different body is refused; after a refusal that sent nothing, you may correct the body and retry with the same key.

When the supplier does not accept responses

A Peppol participant receives Invoice Responses only if it advertises that document type on the network, and many suppliers do not. In that case nothing is sent and the call is refused at once with 422 and the result code received_documents.response_not_accepted_by_sender. Nothing in the request can fix it: tell the supplier by another channel. One exception we have measured: a supplier connected through our own Peppol provider that does not advertise Invoice Responses may have the response accepted without any software reading it, as with the console's test sender below.

respond.ts
import { Peppol, PeppolApiError } from "@getpeppr/sdk";

const peppol = new Peppol({ apiKey: "sk_live_..." });
const receivedDocumentId = "3f7c1a52-9d4e-4c1b-8a2f-6b5e0d1c7a93";

try {
  await peppol.receivedDocuments.respond(receivedDocumentId, { responseCode: "AP" });
} catch (err) {
  if (
    err instanceof PeppolApiError &&
    err.resultCode === "received_documents.response_not_accepted_by_sender"
  ) {
    // Nothing was sent: this sender does not receive Invoice Responses.
    // Tell them another way (email, portal) and record it on your side.
  } else {
    throw err;
  }
}

Following delivery

GET /v1/received-documents/{id}/responses lists the responses sent on a document, oldest first. A response refused at once is not listed — it never left.

  • pending — being sent, or an attempt got no answer yet: retry with the same Idempotency-Key
  • submitted — accepted for transmission
  • delivered — received by the supplier's access point
  • failed — not delivered; failureReason is no_recipient or delivery_failed
  • unconfirmed — an attempt got no answer and its outcome could not be established: contact support before sending again
list-responses.ts
import { Peppol } from "@getpeppr/sdk";

const peppol = new Peppol({ apiKey: "sk_live_..." });
const receivedDocumentId = "3f7c1a52-9d4e-4c1b-8a2f-6b5e0d1c7a93";

const responses = await peppol.receivedDocuments.listResponses(receivedDocumentId);
for (const r of responses) {
  // delivery: "pending" | "submitted" | "delivered" | "failed" | "unconfirmed"
  console.log(r.responseCode, r.delivery, r.failureReason ?? "");
}

Who can respond

  • A standard key responds to documents received by your own legal entity. A sub-tenant's document answers 404, as for every read.
  • A master key also responds for its sub-tenants. The sub-tenant must be able to send: the same rules as send_as apply.
  • In production, responding as your own legal entity requires a verified Peppol identity, and every response passes the same commercial checks as sending an invoice (an active Platform contract, a production access that has not lapsed).
  • In the sandbox, your responses travel on the Peppol test network. Test documents sent from the console's Send a test document button come from a getpeppr test sender that does not advertise Invoice Responses: a response to one is accepted, but no supplier software reads it. To test the full loop, send an invoice between two of your own sandbox legal entities: the sending one must advertise Invoice Responses, which legal entities registered through getpeppr do today. If an older sandbox entity of yours does not, ask support to update it.