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.

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"
}

POST/api/v4/webhooks

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 null if 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 null if 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.

StatusWhen it happensMessage
400The URL is missing. It arrives as a validation failure on Url.La URL es obligatoria.
400The 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.
400The 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.
400The 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.
400The 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.
400There are no event types. It arrives as a validation failure on EnabledEvents.Indica al menos un tipo de evento o '*' para recibirlos todos.
400The wildcard comes with other types. It arrives as a validation failure on EnabledEvents.El comodín '*' no se combina con otros tipos de evento.
400An 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.
400There are more than 100 people. It arrives as a validation failure on PersonIds.Un webhook filtra como máximo 100 personas.
400A person isn't in your tenant. It arrives in details.La persona especificada no es válida o no pertenece a tu organización.
400Your tenant already has 16 URL webhooks. It arrives in details.Se alcanzó el máximo de 16 webhooks.
409Another 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

POST
/api/v4/webhooks
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
}

GET/api/v4/webhooks

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

StatusWhen it happensMessage
400pageSize is outside 1 to 50. It arrives as a validation failure on pageSize.'pageSize' debe estar entre 1 y 50.
400pageNumber is out of range. It arrives as a validation failure on pageNumber.'pageNumber' debe estar entre 1 y 42949672.

Request

GET
/api/v4/webhooks?pageNumber=1&pageSize=10
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/api/v4/webhooks/<id>

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

StatusWhen it happensMessage
404The webhook doesn't exist in your tenant. It arrives in details.No such resource for the given Id: <id>
404The ID isn't a GUID. It arrives in details.Not Found

Request

GET
/api/v4/webhooks/<id>
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
}

PUT/api/v4/webhooks/<id>

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:

StatusWhen it happensMessage
400The ID in the body isn't the one in the path. It arrives as a validation failure on id.IDs mismatches
400The 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.
404The webhook doesn't exist in your tenant. It arrives in details.No such resource for the given Id: <id>
404The ID isn't a GUID. It arrives in details.Not Found
409Another 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

PUT
/api/v4/webhooks/<id>
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/api/v4/webhooks/<id>

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

StatusWhen it happensMessage
404The webhook doesn't exist in your tenant. It arrives in details.No such resource for the given Id: <id>
404The ID isn't a GUID. It arrives in details.Not Found
409Another 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

DELETE
/api/v4/webhooks/<id>
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
}

POST/api/v4/webhooks/<id>/test

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

StatusWhen it happensMessage
400The webhook is disabled. It arrives in details.El webhook está deshabilitado: habilítalo antes de probarlo.
400The 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.
404The webhook doesn't exist in your tenant. It arrives in details.No such resource for the given Id: <id>
404The ID isn't a GUID. It arrives in details.Not Found
429You 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

POST
/api/v4/webhooks/<id>/test
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
}

POST/api/v4/webhooks/<id>/rotate-secret

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

StatusWhen it happensMessage
404The webhook doesn't exist in your tenant. It arrives in details.No such resource for the given Id: <id>
404The ID isn't a GUID. It arrives in details.Not Found
409Another 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

POST
/api/v4/webhooks/<id>/rotate-secret
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
}

GET/api/v4/webhooks/<id>/deliveries

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 null if 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; null if 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 status is "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

StatusWhen it happensMessage
400pageSize is outside 1 to 50. It arrives as a validation failure on pageSize.'pageSize' debe estar entre 1 y 50.
400pageNumber is out of range. It arrives as a validation failure on pageNumber.'pageNumber' debe estar entre 1 y 42949672.
404The 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>
404The ID isn't a GUID. It arrives in details.Not Found

Request

GET
/api/v4/webhooks/<id>/deliveries
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
}

Was this page helpful?