Create a new webhook
curl --request POST \
--url https://api.example.com/organizations/{organizationId}/webhooks \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://example.com/webhooks",
"events": [
"organization.updated",
"member.added"
]
}
'import requests
url = "https://api.example.com/organizations/{organizationId}/webhooks"
payload = {
"url": "https://example.com/webhooks",
"events": ["organization.updated", "member.added"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: 'https://example.com/webhooks',
events: ['organization.updated', 'member.added']
})
};
fetch('https://api.example.com/organizations/{organizationId}/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/organizations/{organizationId}/webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/webhooks',
'events' => [
'organization.updated',
'member.added'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/organizations/{organizationId}/webhooks"
payload := strings.NewReader("{\n \"url\": \"https://example.com/webhooks\",\n \"events\": [\n \"organization.updated\",\n \"member.added\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/organizations/{organizationId}/webhooks")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://example.com/webhooks\",\n \"events\": [\n \"organization.updated\",\n \"member.added\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/organizations/{organizationId}/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://example.com/webhooks\",\n \"events\": [\n \"organization.updated\",\n \"member.added\"\n ]\n}"
response = http.request(request)
puts response.read_bodyWebhooks
Create Webhook
Creates a webhook endpoint for receiving events. Only OWNERS and ADMINS can create webhooks.
POST
/
organizations
/
{organizationId}
/
webhooks
Create a new webhook
curl --request POST \
--url https://api.example.com/organizations/{organizationId}/webhooks \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://example.com/webhooks",
"events": [
"organization.updated",
"member.added"
]
}
'import requests
url = "https://api.example.com/organizations/{organizationId}/webhooks"
payload = {
"url": "https://example.com/webhooks",
"events": ["organization.updated", "member.added"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: 'https://example.com/webhooks',
events: ['organization.updated', 'member.added']
})
};
fetch('https://api.example.com/organizations/{organizationId}/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/organizations/{organizationId}/webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/webhooks',
'events' => [
'organization.updated',
'member.added'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/organizations/{organizationId}/webhooks"
payload := strings.NewReader("{\n \"url\": \"https://example.com/webhooks\",\n \"events\": [\n \"organization.updated\",\n \"member.added\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/organizations/{organizationId}/webhooks")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://example.com/webhooks\",\n \"events\": [\n \"organization.updated\",\n \"member.added\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/organizations/{organizationId}/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://example.com/webhooks\",\n \"events\": [\n \"organization.updated\",\n \"member.added\"\n ]\n}"
response = http.request(request)
puts response.read_bodyCreates a webhook endpoint for receiving events. Only OWNERS and ADMINS can create webhooks.
⚠️ Important: Save the
Authentication
This endpoint requires authentication via Bearer token:- Authorization:
Bearer <token>
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
organizationId | string (UUID) | Yes | Organization ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Webhook endpoint URL (must be valid HTTPS URL) |
events | array | Yes | Array of event types to subscribe to |
Available Events
Subscribe to any of these events:- Organization:
organization.updated - Members:
member.added,member.removed,member.role_updated - Invites:
invite.created,invite.accepted,invite.cancelled - Webhooks:
webhook.created - Invoices (CFDI):
invoice.stamped,invoice.cancelled,invoice.cancellation_pending,invoice.cancellation_rejected,invoice.stamping_failed - Certificates (CSD):
certificate.uploaded,certificate.expiring,certificate.expired,certificate.deleted - Trial:
trial.ending_soon,trial.expired,trial.converted
Permissions
Only OWNERS and ADMINS can create webhooks.Example Request
curl -X POST https://api.timbrix.mx/organizations/550e8400-e29b-41d4-a716-446655440000/webhooks \
-H "Authorization: Bearer <your_token>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks",
"events": ["organization.updated", "member.added"]
}'
Example Response
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"organizationId": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://example.com/webhooks",
"events": ["organization.updated", "member.added"],
"isActive": true,
"secret": "whsec_1234567890abcdef",
"createdAt": "2025-01-26T10:00:00Z",
"updatedAt": "2025-01-26T10:00:00Z"
}
secret value - it’s only returned once and used for signature verification.
Webhook Payload
Your endpoint will receive POST requests with this format:{
"event": "member.added",
"organization": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"slug": "acme-corp",
"name": "Acme Corp"
},
"data": { ... },
"timestamp": "2025-12-26T10:00:00Z"
}
Signature Verification
All webhook requests include an HMAC SHA-256 signature in theX-Webhook-Signature header for verification.
const crypto = require("crypto")
function verifySignature(payload, signature, secret) {
const hmac = crypto.createHmac("sha256", secret)
const digest = hmac.update(payload).digest("hex")
return digest === signature
}
Webhook Event Payloads
invoice.stamped
Sent when a CFDI has been successfully stamped by the PAC.{
"id": "inv_...",
"uuidFiscal": "d3bfbc57-44af-4390-a064-f0afab85e5df",
"tipoComprobante": "I",
"serie": "A",
"folio": "1",
"rfcEmisor": "EKU9003173C9",
"rfcReceptor": "GODE561231GR8",
"total": 116,
"moneda": "MXN",
"environment": "production",
"status": "vigente",
"relatedCfdis": []
}
relatedCfdis is empty for a regular invoice. A substitute CFDI carries [{ "type": "04", "uuids": ["<UUID original>"] }].
invoice.cancelled
Sent when a CFDI has been cancelled (directly or after receiver acceptance).{
"id": "inv_...",
"uuidFiscal": "d3bfbc57-44af-4390-a064-f0afab85e5df",
"motivo": "02",
"folioSustitucion": null,
"environment": "production",
"status": "cancelado"
}
invoice.cancellation_pending
Sent when a CFDI cancellation request requires receiver approval (per SAT eligibility rules) instead of resolving directly. The receiver has untilrespondBy to accept or reject it through the SAT portal — the CFDI remains vigente until then.
{
"id": "inv_...",
"uuidFiscal": "d3bfbc57-44af-4390-a064-f0afab85e5df",
"motivo": "02",
"respondBy": "2026-09-22T15:50:03.412Z",
"environment": "production",
"status": "vigente"
}
invoice.cancellation_rejected
Sent when a receiver rejects a pending CFDI cancellation request. The CFDI remainsvigente.
{
"id": "inv_...",
"uuidFiscal": "d3bfbc57-44af-4390-a064-f0afab85e5df",
"motivo": "02",
"environment": "production",
"status": "vigente"
}
invoice.stamping_failed
Sent when CFDI stamping failed at the PAC.{
"environment": "production",
"serie": "A",
"folio": "1",
"tipoComprobante": "I",
"rfcReceptor": "GODE561231GR8",
"errorMessage": "El PAC rechazó el comprobante: CFDI40149",
"errorCode": null
}
certificate.uploaded
Sent when a CSD is uploaded.replaced is true when it replaced an existing CSD (rotation), and previousSerialNumber holds the old serial. Re-uploading the same CSD also reports replaced: true, with previousSerialNumber equal to serialNumber. rfc may be null if the organization has no legal data on file.
{
"rfc": "EKU9003173C9",
"serialNumber": "30001000000500003416",
"expiresAt": "2027-05-18T11:43:51.000Z",
"replaced": true,
"previousSerialNumber": "30001000000400002434"
}
certificate.expiring
Sent once per threshold, 30, 7 and 1 days before the CSD expires (checked daily at 9:00 Mexico City time).daysUntilExpiration counts full days remaining. If several thresholds were crossed since the last check, only the closest one is sent. Uploading a new CSD resets the alerts. daysUntilExpiration can be lower than threshold (for example threshold: 1 with daysUntilExpiration: 0 when it expires later the same day, or when a CSD is uploaded that is already within 30 days of expiring). rfc may be null if the organization has no legal data on file.
{
"rfc": "EKU9003173C9",
"serialNumber": "30001000000500003416",
"expiresAt": "2027-05-18T11:43:51.000Z",
"daysUntilExpiration": 7,
"threshold": 7
}
certificate.expired
Sent once when the CSD has expired. From this point stamping fails until a new CSD is uploaded.rfc may be null if the organization has no legal data on file.
{
"rfc": "EKU9003173C9",
"serialNumber": "30001000000500003416",
"expiresAt": "2027-05-18T11:43:51.000Z"
}
certificate.deleted
Sent when the organization’s CSD is deleted. Stamping fails until a new one is uploaded.rfc may be null if the organization has no legal data on file.
{
"rfc": "EKU9003173C9",
"serialNumber": "30001000000500003416"
}
trial.ending_soon
Sent once when 3 days or less remain in the organization’s free trial.daysRemaining is rounded up (1–3).
{
"trialStartedAt": "2026-10-01T15:00:00.000Z",
"trialEndsAt": "2026-10-15T15:00:00.000Z",
"daysRemaining": 3,
"trialPlan": "pro"
}
trial.expired
Sent once when the trial ended without a paid plan and access was locked.expiredAt is when the lock was applied.
{
"trialStartedAt": "2026-10-01T15:00:00.000Z",
"trialEndsAt": "2026-10-15T15:00:00.000Z",
"expiredAt": "2026-10-16T15:00:02.000Z"
}
trial.converted
Sent once when a trial organization subscribes to a paid plan.afterExpiration is true when the payment happened after the trial had already ended.
{
"trialStartedAt": "2026-10-01T15:00:00.000Z",
"trialEndsAt": "2026-10-15T15:00:00.000Z",
"plan": "business",
"convertedAt": "2026-10-09T18:22:41.000Z",
"afterExpiration": false
}
There is no
trial.started event: the trial begins the moment the
organization is created, before any webhook can be registered. Read the trial
dates from GET /organizations/{id} instead.Retry Logic
Failed webhook deliveries are retried up to 3 times with exponential backoff (1s, 5s, 30s).Common Errors
400 Bad Request
Invalid input data.{
"statusCode": 400,
"message": ["url must be a valid URL", "events must be an array"]
}
401 Unauthorized
Authentication required.{
"statusCode": 401,
"message": "Authentication required. Provide a valid bearer token."
}
403 Forbidden
Only owners and admins can create webhooks.{
"statusCode": 403,
"message": "Access denied. Only owners and admins can create webhooks."
}
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
Body
application/json
Webhook endpoint URL
Example:
"https://example.com/webhooks"
Events to subscribe to
Available options:
organization.updated, member.added, member.removed, member.role_updated, invite.created, invite.accepted, invite.cancelled, webhook.created, invoice.stamped, invoice.cancelled, invoice.cancellation_pending, invoice.cancellation_rejected, invoice.stamping_failed, certificate.uploaded, certificate.expiring, certificate.expired, certificate.deleted, trial.ending_soon, trial.expired, trial.converted Example:
["organization.updated", "member.added"]
Response
Webhook created successfully. Returns webhook configuration with signing secret.