Read customer replies
How replies of customers about your invoices reach you, through the invoice.reply.received webhook and the invoice-replies endpoints, what is masked in each, and how to download their attachments.
A reply is a message in which a customer answers about one of your invoices. BeeL. stores replies from two sources and gives them one shape:
- the ones it receives itself through a connection, for example the email
replies captured by
beel_email(see Capture replies by email); - the ones you register from your own channels.
Each reply has a channel (the medium the customer wrote in), a connection_id (null when you
registered it) and authenticated_by, which says what links the message to its invoice:
REPLY_TOKEN for an answer to an invoice email, API_KEY for one you registered. It never says who
wrote it.
Get them as they arrive
Subscribe a webhook to invoice.reply.received. It is delivered when a reply is stored,
also when you register it yourself. Subscribing needs the invoice-replies:read scope, and the event
only reaches the endpoints of the account that owns the company, never those of an account that
manages it.
{
"id": "9b2f4c1d-7e3a-4b8c-9d0e-1f2a3b4c5d6e",
"type": "invoice.reply.received",
"created_at": "2026-11-03T09:12:45Z",
"api_version": "2025-01",
"livemode": true,
"company_id": "9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"nif": "B12345674",
"data": {
"reply_id": "3f0c9a1e-6b2d-4c8f-a1e5-7d9b0c2e4f61",
"channel": "EMAIL",
"connection_id": "8c3e1f7a-2b4d-4e6f-9a1c-5d7b3e9f2a41",
"invoice_id": "b71d5e2a-9c3f-4a7b-8e10-2f6d4c8a9b13",
"invoice_number": "F2026-0142",
"authenticated_by": "REPLY_TOKEN",
"customer_id": "7a2c9e14-3b5d-4f6a-8c0e-1d2b3a4c5e6f",
"received_at": "2026-11-03T09:12:44Z",
"sender": { "address": "[EMAIL]" },
"subject": "RE: Factura F2026-0142",
"excerpt": "Hola, el CIF que aparece no es el nuestro, es [NIF]. ¿Podéis corregirla?",
"attachments": []
}
}The event page has the full schema. Events do not necessarily arrive in the order they happened. Deduplicate and verify the signature as for any other webhook.
Read them on demand
- List the replies of a company, most recent first,
filtering by
invoice_id,connection_id,customer_id,channel(a comma-separated list),from,toandsort_order. A reply you registered has no connection, so aconnection_idfilter never returns it. - Get one reply with its full text.
Both need invoice-replies:read. See Permissions.
What is masked
Replies carry personal data of your customers, and often of third parties. The webhook and the list mask what they show, and only the detail of one reply gives you the original text.
| Field | Webhook and list | Detail of one reply |
|---|---|---|
excerpt | First paragraph of the text, up to 280 characters, without the quoted original email. Tax IDs, IBANs, email addresses and phone numbers become [NIF], [IBAN], [EMAIL] and [PHONE]. | The same excerpt. |
text | Not included. | The full text, unmasked, up to 20,000 characters; is_text_truncated tells you when it was cut. |
subject | Masked in the same way. | The same masked subject. |
sender.address | [EMAIL], [PHONE] or [ID], by channel. | The same masked address. |
sender.name | Never included. | The display name, when the channel carries one. |
external_id | Not included. | The identifier of the message on its channel. |
attachments | id, url, content_type, size_bytes and file_expires_at. | Also the filename. |
BeeL. does not store the files of the attachments: see Attachments.
sender is the address the message says it comes from, and it is not verified. Do not use it alone
to authorize a change.
Attachments
Each attachment of a reply carries an id, a url, its content_type, its size_bytes and
file_expires_at, the same in the invoice.reply.received webhook, in the list and in the detail of
the reply. The detail also gives the filename.
"attachments": [
{
"id": "5b0e2c7a-91d4-4f3e-8a6b-2c1d9e0f7a34",
"url": "/v1/companies/9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f/invoice-replies/3f0c9a1e-6b2d-4c8f-a1e5-7d9b0c2e4f61/attachments/5b0e2c7a-91d4-4f3e-8a6b-2c1d9e0f7a34",
"content_type": "application/pdf",
"size_bytes": 48213,
"file_expires_at": "2026-11-18T10:15:00Z"
}
]BeeL. does not store the file. It can be downloaded for a few days after the reply arrives, until
around file_expires_at, through a temporary link that BeeL. requests when you ask for it.
Download a file
- Call the
urlof the attachment with your API key. It is relative to the API base URL,https://app.beel.es/api, and points toGET /v1/companies/{company_id}/invoice-replies/{reply_id}/attachments/{attachment_id}, which needsinvoice-replies:read. - The response carries
download_url, the temporary link to the file, anddownload_url_expires_at, about an hour later. - Download
download_urlbeforedownload_url_expires_at, without your API key: the link goes to the file, not to the BeeL. API, so do not send it theAuthorizationheader.
# 1. Ask for a download link
curl "https://app.beel.es/api$ATTACHMENT_URL" \
-H "Authorization: Bearer $BEEL_API_KEY"
# 2. Download the file with the download_url of the response, without the API key
curl -o justificante-transferencia.pdf "$DOWNLOAD_URL"Do not keep download_url: when you need the file again, call url again for a new link, while the
file is still available.
When there is no file
| Value | Meaning |
|---|---|
file_expires_at: null | The attachment never had a file: it was registered with POST .../invoice-replies, which stores only its description. Its download_url is always null. |
download_url: null | There is no file to download, and download_url_expires_at is null too. Either the attachment never had one, or its file is no longer available. That is the case after file_expires_at, and it can happen earlier, because file_expires_at is an estimate. Calling again does not bring the file back. |
attachments: null | The reply is past its retention of 12 months after received_at and its attachments were erased. Asking for one of them answers 404 INVOICE_REPLY_ATTACHMENT_NOT_FOUND. |
Errors
| Status | Code | What to do |
|---|---|---|
404 | INVOICE_REPLY_NOT_FOUND | The reply does not exist, belongs to another company or environment, or was deleted. |
404 | INVOICE_REPLY_ATTACHMENT_NOT_FOUND | The reply has no attachment with that id, or its attachments were erased. Use the url as it comes in the reply. |
429 | TOO_MANY_REQUESTS | Too many download links were requested in a short time. Wait the seconds in the Retry-After header and call again. |
429 | RATE_LIMIT_EXCEEDED | Your key went over its rate limit. Wait the seconds in Retry-After and call again. |
502 | EXTERNAL_SERVICE_ERROR | The link could not be requested. It is transient: call again later, before file_expires_at. |
A missing scope or access answers 403, as in Permissions.
Keep the files you need
BeeL. never keeps a copy of a file. If you need to keep an attachment, download it when
invoice.reply.received arrives: for each attachment whose file_expires_at is not null, call its
url and save the file in your own storage. Once the file is gone, neither BeeL. nor the API can
recover it.
Fields that can be null
invoice_id, invoice_number and customer_id can be null (a customer is null when the invoice
has a one-off recipient), and subject is null on every channel but EMAIL. After the
retention period the content fields are null too. Handle
null in each of them.
Related
Capture replies by email
Connect beel_email and BeeL. keeps a copy of the replies your customers send to the emails of your invoices. What changes in those emails and what is not captured.
Register replies from other channels
Send BeeL. the replies your customers write on WhatsApp, Slack, email or your own product, so they are stored next to their invoice with the same shape and webhook.