Webhooks
A webhook is an https URL in your system that receives, by POST, the events of your tenant (the space in Fiscalapi that holds your issuers, recipients, credits, and data), signed with its secret. Use these endpoints to register it, change it, test it, rotate its secret, and review its delivery log.
See the introduction to webhooks for the event types, signature verification, retries, and secret rotation.
Dates and errors
Managing webhooks requires the Webhooks.Manage permission. The owner (the person who created the tenant) and the other people who sign in to your tenant have it. The API accepts your API key or your dashboard session.
Webhook and delivery dates come in the time zone of the X-TIME-ZONE header, with no offset and in the format yyyy-MM-ddTHH:mm:ss.fff; without that header, they come in UTC with a trailing Z. The event envelope always comes in UTC with a trailing Z, including in List events.
In a 4xx error response, message is "Sorry, it's not me, it's you." except in a 429, and the cause is in details, or in data for a validation failure. Without a valid API key or session, the API responds with 401; without the Webhooks.Manage permission, it responds with 403 and "No tienes permiso para administrar webhooks." (You don't have permission to manage webhooks).
Test a webhook, Resend an event, and Trigger a test event have a rate limit per API key. If you exceed it, the API responds with 429 and the Retry-After header, in seconds, and doesn't run the operation:
Response
{
"data": null,
"succeeded": false,
"message": "Too Many Requests",
"details": "Se excedió el límite de solicitudes de la familia BusinessWrite (600 por 60s). Reintenta en 12 segundos. Los headers RateLimit-Limit, RateLimit-Remaining y RateLimit-Reset indican tu consumo actual en cada respuesta.",
"httpStatusCode": 429,
"traceIdentifier": "8D2F6A41C07B4E9DA35E1F0B6C7E2A93"
}
Create a webhook
This endpoint registers a URL that receives the subscribed events of your tenant, or of the people (person: any individual or legal entity registered in your tenant) you specify. It responds with 201, the webhook, and its signing secret.
Request body
- Name
url- Type
- string
- required
- Description
The https URL that receives the deliveries. See the rules in Register a webhook.
- Name
description- Type
- string?
- Description
A description of up to 256 characters.
- Name
enabledEvents- Type
- array of strings
- required
- Description
The event types it receives, or only
"*"for all of them. See Event types.
- Name
personIds- Type
- array of strings?
- Description
The IDs of the people whose events it receives, up to 100. If omitted or empty, it receives the events of all of them.
Response
- Name
id- Type
- string?
- Description
The webhook ID.
- Name
kind- Type
- string?
- Description
"url"for a URL webhook.
- Name
url- Type
- string?
- Description
The URL that receives the deliveries.
- Name
description- Type
- string?
- Description
The webhook's description, or
""if it has none.
- Name
enabledEvents- Type
- array of strings?
- Description
The subscribed event types, or only
"*".
- Name
personIds- Type
- array of strings?
- Description
The people whose events it receives, or
nullif it receives the events of all of them.
- Name
status- Type
- string?
- Description
"enabled"or"disabled".
- Name
disabledReason- Type
- string?
- expandable
- Description
Why it's disabled, or
null.- Type
- enum:
- Values
- "manual""gone""failingTooLong"
- Name
disabledAt- Type
- string?
- Description
When it was disabled, or
null.
- Name
failingSince- Type
- string?
- Description
Since when the webhook has been failing, or
nullif it isn't failing. Only the 2xx for a real event clears it, not the 2xx for a test.
- Name
listenerPersonId- Type
- string?
- Description
Only for CLI listeners. On a URL webhook, it's
null.
- Name
sessionExpiresAt- Type
- string?
- Description
Only for CLI listeners. On a URL webhook, it's
null.
- Name
createdAt- Type
- string?
- Description
The date and time the webhook was created.
- Name
updatedAt- Type
- string?
- Description
The date and time of the last change. On creation, it's the same as
createdAt.
- Name
secret- Type
- string?
- Description
The signing secret:
whsec_followed by 44 base64 characters. Save it: with the API key, it's only returned when you create the webhook and when you rotate the secret.
Errors
A validation failure arrives in data as a ValidationFailure, with the field that caused it; the others arrive with the message in details. In the event type message, <tipo> is the type you sent.
| Status | When it happens | Message |
|---|---|---|
| 400 | The URL is missing. It arrives as a validation failure on Url. | La URL es obligatoria. |
| 400 | The URL is longer than 2,048 characters. It arrives as a validation failure on Url. | La URL no puede tener más de 2048 caracteres. |
| 400 | The URL isn't https, isn't absolute, or includes a username and password. It arrives as a validation failure on Url. | La URL del webhook debe ser https, absoluta y sin usuario ni contraseña. |
| 400 | The URL points to a private, local, or reserved address, or to localhost. It arrives as a validation failure on Url. | La URL del webhook apunta a una dirección no permitida. |
| 400 | The description is longer than 256 characters. It arrives as a validation failure on Description. | La descripción no puede tener más de 256 caracteres. |
| 400 | There are no event types. It arrives as a validation failure on EnabledEvents. | Indica al menos un tipo de evento o '*' para recibirlos todos. |
| 400 | The wildcard comes with other types. It arrives as a validation failure on EnabledEvents. | El comodín '*' no se combina con otros tipos de evento. |
| 400 | An event type doesn't exist. It arrives as a validation failure on that element of EnabledEvents, for example EnabledEvents[1]. | El tipo de evento '<tipo>' no existe. |
| 400 | There are more than 100 people. It arrives as a validation failure on PersonIds. | Un webhook filtra como máximo 100 personas. |
| 400 | A person isn't in your tenant. It arrives in details. | La persona especificada no es válida o no pertenece a tu organización. |
| 400 | Your tenant already has 16 URL webhooks. It arrives in details. | Se alcanzó el máximo de 16 webhooks. |
| 409 | Another change to your webhooks is in progress. Repeat the request. It arrives in details. | Otro cambio a los webhooks está en curso; intenta de nuevo. |
Request
curl --location 'https://test.fiscalapi.com/api/v4/webhooks' \
--header 'X-TENANT-KEY: <tenant_key>' \
--header 'X-TIME-ZONE: America/Mexico_City' \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: <api_key>' \
--data-raw '{
"url": "https://www.example.com/webhooks/fiscalapi",
"description": "ERP producción",
"enabledEvents": ["invoice.canceled", "invoice.cancellation_rejected"],
"personIds": ["4f6a2c1e-8b3d-4e5f-9a7b-1c2d3e4f5a6b"]
}'
Response
{
"data": {
"secret": "whsec_ZWplbXBsby1kZS1zZWNyZXRvLWRlLWZpcm1hLTAwMDE=",
"id": "6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0",
"kind": "url",
"url": "https://www.example.com/webhooks/fiscalapi",
"description": "ERP producción",
"enabledEvents": ["invoice.canceled", "invoice.cancellation_rejected"],
"personIds": ["4f6a2c1e-8b3d-4e5f-9a7b-1c2d3e4f5a6b"],
"status": "enabled",
"disabledReason": null,
"disabledAt": null,
"failingSince": null,
"listenerPersonId": null,
"sessionExpiresAt": null,
"createdAt": "2026-10-13T09:15:00.000",
"updatedAt": "2026-10-13T09:15:00.000"
},
"succeeded": true,
"message": "",
"details": "",
"httpStatusCode": 201
}
List webhooks
This endpoint returns your tenant's webhooks in the order they were created, paginated and without their secret. Each item has the properties of the Create a webhook response, except secret.
The list also includes the fiscalapi listen listeners, with kind set to "cliListener" and the URL set to null. Each listener is visible only to the person it belongs to and to the tenant owner; for everyone else, it doesn't exist.
For a grid, GET /api/v4/webhooks/load returns the same webhooks with the DevExtreme load options (filtering, sorting, paging, and grouping) and responds with their LoadResult, without the response envelope. It validates its parameters like the other load queries; see ValidationFailure.
Query parameters
- Name
pageNumber- Type
- int?
- Description
The page number.
Default:1
- Name
pageSize- Type
- int?
- Description
Webhooks per page, from 1 to 50.
Default:10
Errors
| Status | When it happens | Message |
|---|---|---|
| 400 | pageSize is outside 1 to 50. It arrives as a validation failure on pageSize. | 'pageSize' debe estar entre 1 y 50. |
| 400 | pageNumber is out of range. It arrives as a validation failure on pageNumber. | 'pageNumber' debe estar entre 1 y 42949672. |
Request
curl --location 'https://test.fiscalapi.com/api/v4/webhooks?pageNumber=1&pageSize=10' \
--header 'X-TENANT-KEY: <tenant_key>' \
--header 'X-TIME-ZONE: America/Mexico_City' \
--header 'X-API-KEY: <api_key>'
Response
{
"data": {
"items": [
{
"id": "6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0",
"kind": "url",
"url": "https://www.example.com/webhooks/fiscalapi",
"description": "ERP producción",
"enabledEvents": ["invoice.canceled", "invoice.cancellation_rejected"],
"personIds": ["4f6a2c1e-8b3d-4e5f-9a7b-1c2d3e4f5a6b"],
"status": "enabled",
"disabledReason": null,
"disabledAt": null,
"failingSince": null,
"listenerPersonId": null,
"sessionExpiresAt": null,
"createdAt": "2026-10-13T09:15:00.000",
"updatedAt": "2026-10-13T09:15:00.000"
}
],
"pageNumber": 1,
"totalPages": 1,
"totalCount": 1,
"hasPreviousPage": false,
"hasNextPage": false
},
"succeeded": true,
"message": "",
"details": "",
"httpStatusCode": 200
}
Get a webhook
This endpoint returns one of your tenant's webhooks with its status, without its secret. It has the properties of the Create a webhook response, except secret.
Errors
| Status | When it happens | Message |
|---|---|---|
| 404 | The webhook doesn't exist in your tenant. It arrives in details. | No such resource for the given Id: <id> |
| 404 | The ID isn't a GUID. It arrives in details. | Not Found |
Request
curl --location 'https://test.fiscalapi.com/api/v4/webhooks/6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0' \
--header 'X-TENANT-KEY: <tenant_key>' \
--header 'X-TIME-ZONE: America/Mexico_City' \
--header 'X-API-KEY: <api_key>'
Response
{
"data": {
"id": "6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0",
"kind": "url",
"url": "https://www.example.com/webhooks/fiscalapi",
"description": "ERP producción",
"enabledEvents": ["invoice.canceled", "invoice.cancellation_rejected"],
"personIds": ["4f6a2c1e-8b3d-4e5f-9a7b-1c2d3e4f5a6b"],
"status": "enabled",
"disabledReason": null,
"disabledAt": null,
"failingSince": null,
"listenerPersonId": null,
"sessionExpiresAt": null,
"createdAt": "2026-10-13T09:15:00.000",
"updatedAt": "2026-10-13T09:15:00.000"
},
"succeeded": true,
"message": "",
"details": "",
"httpStatusCode": 200
}
Replace a webhook
This endpoint replaces a webhook's URL, description, event types, and people, and enables or disables it with status. Send the full model, with the webhook's id equal to the one in the path.
Enabling a disabled webhook clears failingSince, disabledReason, and disabledAt. Disabling it closes its pending deliveries. The response is the webhook, without its secret.
Request body
- Name
id- Type
- string
- required
- Description
The webhook ID, the same as in the path.
- Name
url- Type
- string
- required
- Description
The https URL that receives the deliveries.
- Name
description- Type
- string?
- Description
A description of up to 256 characters.
- Name
enabledEvents- Type
- array of strings
- required
- Description
The event types it receives, or only
"*".
- Name
personIds- Type
- array of strings?
- Description
The people whose events it receives. If omitted or empty, it receives the events of all of them.
- Name
status- Type
- string?
- Description
"enabled"or"disabled". If omitted, the webhook keeps its status.
Errors
It returns the same URL, description, event type, and people errors as Create a webhook, in the same place in the response, except the 16-webhook limit. In addition:
| Status | When it happens | Message |
|---|---|---|
| 400 | The ID in the body isn't the one in the path. It arrives as a validation failure on id. | IDs mismatches |
| 400 | The webhook is a CLI listener. It arrives in details. | Una escucha de CLI no se edita ni se prueba; usa fiscalapi listen y fiscalapi trigger. |
| 404 | The webhook doesn't exist in your tenant. It arrives in details. | No such resource for the given Id: <id> |
| 404 | The ID isn't a GUID. It arrives in details. | Not Found |
| 409 | Another change to your webhooks is in progress. Repeat the request. It arrives in details. | Otro cambio a los webhooks está en curso; intenta de nuevo. |
Request
curl --location --request PUT 'https://test.fiscalapi.com/api/v4/webhooks/6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0' \
--header 'X-TENANT-KEY: <tenant_key>' \
--header 'X-TIME-ZONE: America/Mexico_City' \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: <api_key>' \
--data-raw '{
"id": "6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0",
"url": "https://www.example.com/webhooks/fiscalapi",
"description": "ERP producción",
"enabledEvents": ["*"],
"personIds": [],
"status": "enabled"
}'
Response
{
"data": {
"id": "6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0",
"kind": "url",
"url": "https://www.example.com/webhooks/fiscalapi",
"description": "ERP producción",
"enabledEvents": ["*"],
"personIds": null,
"status": "enabled",
"disabledReason": null,
"disabledAt": null,
"failingSince": null,
"listenerPersonId": null,
"sessionExpiresAt": null,
"createdAt": "2026-10-13T09:15:00.000",
"updatedAt": "2026-10-13T10:02:31.000"
},
"succeeded": true,
"message": "",
"details": "",
"httpStatusCode": 200
}
Delete a webhook
This endpoint deletes a webhook, whether a URL webhook or a fiscalapi listen listener, and closes its pending deliveries. Its delivery log is kept, and you can still review it with List deliveries.
Errors
| Status | When it happens | Message |
|---|---|---|
| 404 | The webhook doesn't exist in your tenant. It arrives in details. | No such resource for the given Id: <id> |
| 404 | The ID isn't a GUID. It arrives in details. | Not Found |
| 409 | Another change to your webhooks is in progress. Repeat the request. It arrives in details. | Otro cambio a los webhooks está en curso; intenta de nuevo. |
Request
curl --location --request DELETE 'https://test.fiscalapi.com/api/v4/webhooks/6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0' \
--header 'X-TENANT-KEY: <tenant_key>' \
--header 'X-TIME-ZONE: America/Mexico_City' \
--header 'X-API-KEY: <api_key>'
Response
{
"data": true,
"succeeded": true,
"message": "",
"details": "",
"httpStatusCode": 200
}
Test a webhook
This endpoint records a webhook.test event and delivers it only to this webhook, signed and in a single attempt, with no retries. Its 2xx doesn't count toward the webhook's health; see Test your endpoint. It responds with 202 and the event and delivery IDs; follow the result in List deliveries.
Response
- Name
eventId- Type
- string?
- Description
The test event ID.
- Name
deliveryId- Type
- string?
- Description
The delivery ID in the webhook's delivery log.
Errors
| Status | When it happens | Message |
|---|---|---|
| 400 | The webhook is disabled. It arrives in details. | El webhook está deshabilitado: habilítalo antes de probarlo. |
| 400 | The webhook is a CLI listener. It arrives in details. | Una escucha de CLI no se edita ni se prueba; usa fiscalapi listen y fiscalapi trigger. |
| 404 | The webhook doesn't exist in your tenant. It arrives in details. | No such resource for the given Id: <id> |
| 404 | The ID isn't a GUID. It arrives in details. | Not Found |
| 429 | You exceeded your API key's rate limit. It arrives in message, with the detail in details and the Retry-After header. See Rate limit. | Too Many Requests |
Request
curl --location --request POST 'https://test.fiscalapi.com/api/v4/webhooks/6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0/test' \
--header 'X-TENANT-KEY: <tenant_key>' \
--header 'X-TIME-ZONE: America/Mexico_City' \
--header 'X-API-KEY: <api_key>'
Response
{
"data": {
"eventId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
"deliveryId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"
},
"succeeded": true,
"message": "",
"details": "",
"httpStatusCode": 202
}
Rotate a secret
This endpoint generates a new signing secret and returns it in secret, along with the webhook. The previous secret keeps signing every delivery for 24 hours. See Rotate the secret.
Errors
| Status | When it happens | Message |
|---|---|---|
| 404 | The webhook doesn't exist in your tenant. It arrives in details. | No such resource for the given Id: <id> |
| 404 | The ID isn't a GUID. It arrives in details. | Not Found |
| 409 | Another change to your webhooks is in progress. Repeat the request. It arrives in details. | Otro cambio a los webhooks está en curso; intenta de nuevo. |
Request
curl --location --request POST 'https://test.fiscalapi.com/api/v4/webhooks/6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0/rotate-secret' \
--header 'X-TENANT-KEY: <tenant_key>' \
--header 'X-TIME-ZONE: America/Mexico_City' \
--header 'X-API-KEY: <api_key>'
Response
{
"data": {
"secret": "whsec_ZWplbXBsby1kZS1zZWNyZXRvLXJvdGFkby0wMDAwMDI=",
"id": "6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0",
"kind": "url",
"url": "https://www.example.com/webhooks/fiscalapi",
"description": "ERP producción",
"enabledEvents": ["*"],
"personIds": null,
"status": "enabled",
"disabledReason": null,
"disabledAt": null,
"failingSince": null,
"listenerPersonId": null,
"sessionExpiresAt": null,
"createdAt": "2026-10-13T09:15:00.000",
"updatedAt": "2026-10-13T11:40:12.000"
},
"succeeded": true,
"message": "",
"details": "",
"httpStatusCode": 200
}
List deliveries
This endpoint returns a webhook's delivery log, newest first, even if the webhook was deleted. It only includes deliveries of events from the last 30 days.
For a grid, GET /api/v4/webhooks/<id>/deliveries/load returns the same deliveries with the DevExtreme load options and responds with their LoadResult, without the response envelope; see ValidationFailure.
Query parameters
- Name
pageNumber- Type
- int?
- Description
The page number.
Default:1
- Name
pageSize- Type
- int?
- Description
Deliveries per page, from 1 to 50.
Default:10
Response
- Name
id- Type
- string?
- Description
The delivery ID.
- Name
createdAt- Type
- string?
- Description
When the delivery was created.
- Name
eventId- Type
- string?
- Description
The ID of the delivered event. Retrieve the event with Get an event.
- Name
kind- Type
- string?
- expandable
- Description
What triggered the delivery.
- Type
- enum:
- Values
- "automatic""manual""test"
- Name
status- Type
- string?
- expandable
- Description
The delivery status.
- Type
- enum:
- Values
- "pending""succeeded""failed"
- Name
attempts- Type
- int?
- Description
The number of attempts made.
- Name
lastStatusCode- Type
- int?
- Description
The HTTP status code of the last attempt, or
nullif there was no response. A timeout while reading the response body keeps the code that did arrive.
- Name
lastError- Type
- string?
- expandable
- Description
Why the last attempt didn't get a complete response, or why the delivery was closed without being sent;
nullif there was a complete response. Treat it as free text: it can have other values. For a CLI listener, it's the error text the CLI reported, up to 64 characters. These are the current values:- Type
- enum:
- Values
- "Timeout""ConnectionFailed""TlsFailed"
- Name
lastResponseExcerpt- Type
- string?
- Description
The first KB of your endpoint's last response.
- Name
eventType- Type
- string?
- Description
The type of the delivered event.
- Name
nextAttemptAt- Type
- string?
- Description
When the next attempt is due. It only matters while
statusis"pending"; on a finished delivery, ignore it.
- Name
lastAttemptAt- Type
- string?
- Description
When the last attempt was made, or
null.
- Name
lastLatencyMs- Type
- int?
- Description
How many milliseconds the last attempt took, or
null.
Errors
| Status | When it happens | Message |
|---|---|---|
| 400 | pageSize is outside 1 to 50. It arrives as a validation failure on pageSize. | 'pageSize' debe estar entre 1 y 50. |
| 400 | pageNumber is out of range. It arrives as a validation failure on pageNumber. | 'pageNumber' debe estar entre 1 y 42949672. |
| 404 | The webhook doesn't exist in your tenant, not even as a deleted webhook. It arrives in details. | No such resource for the given Id: <id> |
| 404 | The ID isn't a GUID. It arrives in details. | Not Found |
Request
curl --location 'https://test.fiscalapi.com/api/v4/webhooks/6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0/deliveries?pageNumber=1&pageSize=10' \
--header 'X-TENANT-KEY: <tenant_key>' \
--header 'X-TIME-ZONE: America/Mexico_City' \
--header 'X-API-KEY: <api_key>'
Response
{
"data": {
"items": [
{
"id": "e4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
"eventId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"eventType": "invoice.canceled",
"kind": "automatic",
"status": "succeeded",
"attempts": 2,
"nextAttemptAt": "2026-10-12T12:47:58.000",
"lastAttemptAt": "2026-10-12T12:48:41.000",
"lastStatusCode": 200,
"lastLatencyMs": 184,
"lastError": null,
"lastResponseExcerpt": "{\"received\":true}",
"createdAt": "2026-10-12T12:47:53.000"
}
],
"pageNumber": 1,
"totalPages": 1,
"totalCount": 1,
"hasPreviousPage": false,
"hasNextPage": false
},
"succeeded": true,
"message": "",
"details": "",
"httpStatusCode": 200
}