Webhooks
Webhooks let you receive real-time notifications when events happen in Breakwater — licenses created, customers updated, tokens revoked, and more. Instead of polling the API, Breakwater sends an HTTP POST to your endpoint whenever a subscribed event occurs.
What is a Webhook Endpoint?
A webhook endpoint is an HTTPS URL on your server that Breakwater sends event data to. Each endpoint has:
- URL: Where to send the events
- Event subscriptions: Which event types to receive
- Signing secret: A shared secret used to verify that requests came from Breakwater
- Enabled/disabled status: Whether the endpoint is currently receiving events
You can register multiple endpoints to route different events to different systems — for example, license events to your CRM and auth token events to your security dashboard.
Creating an Endpoint
- Navigate to Webhooks in the vendor portal
- Click New Webhook Endpoint
- Fill in the endpoint details:
- URL (required): The HTTPS URL that will receive events
- Name: A descriptive name for the endpoint
- Description: Optional notes about its purpose
- Event types: Check the events you want this endpoint to receive
- Click Create Webhook Endpoint
- Copy the signing secret immediately — it won't be shown again
The signing secret is displayed once after creation. Store it securely in your receiving application's configuration.
Event Types
Events are organized by resource. When creating or editing an endpoint, select which events it should receive:
Licenses
| Event | Fired when |
|---|---|
license.created |
A new license is created |
license.activated |
A license transitions to active status |
license.expired |
A license passes its expiration date |
license.cancelled |
A license is cancelled |
Customers
| Event | Fired when |
|---|---|
customer.created |
A new customer is created |
customer.updated |
A customer's details are updated |
customer.deleted |
A customer is deleted |
Auth Tokens
| Event | Fired when |
|---|---|
auth_token.created |
A new auth token is issued |
auth_token.revoked |
An auth token is revoked |
Products
| Event | Fired when |
|---|---|
product.created |
A new product is created |
product.updated |
A product is updated |
product.deleted |
A product is deleted |
Repositories
| Event | Fired when |
|---|---|
repository.created |
A new repository is created |
repository.deleted |
A repository is deleted |
Payload Format
Every webhook delivery is an HTTP POST with a JSON body using this envelope structure:
{
"id": "whev_abc123",
"type": "license.created",
"created_at": "2026-02-24T12:00:00Z",
"data": {
"id": 42,
"product_id": "prod_abc123",
"customer_id": "cust_def456",
"status": "pending",
"starts_at": "2026-03-01T00:00:00Z",
"expires_at": "2027-03-01T00:00:00Z"
},
"links": {
"api_url": "https://app.breakwater.dev/api/v1/vendor/customers/cust_def456/licenses/lic_ghi789"
}
}
| Field | Description |
|---|---|
id |
Unique event identifier |
type |
The event type string |
created_at |
When the event occurred (ISO 8601) |
data |
The full serialized resource at the time of the event |
links.api_url |
API URL to fetch the current state of the resource |
The data field contains the same structure as the corresponding API response, so you can process events without making follow-up API calls. If you need the latest state (e.g., it may have changed since the event), use the links.api_url to fetch it.
Verifying Signatures
Every webhook request includes two headers:
| Header | Description |
|---|---|
X-Breakwater-Signature |
Hex-encoded HMAC-SHA256 signature |
X-Breakwater-Timestamp |
Unix timestamp of when the request was signed |
The signed content is the timestamp and the raw request body joined by a period: {timestamp}.{body}.
To verify a webhook:
- Extract the
X-Breakwater-SignatureandX-Breakwater-Timestampheaders - Construct the signed content:
"{timestamp}.{raw_body}" - Compute the HMAC-SHA256 of the signed content using your signing secret
- Compare your computed signature with the one in the header using a constant-time comparison
Ruby
def verify_webhook(request, signing_secret)
signature = request.headers["X-Breakwater-Signature"]
timestamp = request.headers["X-Breakwater-Timestamp"]
body = request.body.read
expected = OpenSSL::HMAC.hexdigest("SHA256", signing_secret, "#{timestamp}.#{body}")
ActiveSupport::SecurityUtils.secure_compare(signature, expected)
end
Node.js
const crypto = require("crypto");
function verifyWebhook(body, signature, timestamp, signingSecret) {
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Python
import hashlib, hmac
def verify_webhook(body, signature, timestamp, signing_secret):
expected = hmac.new(
signing_secret.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Replay Protection
To guard against replay attacks, reject requests where the timestamp is more than a few minutes old:
timestamp = request.headers["X-Breakwater-Timestamp"].to_i
if Time.now.to_i - timestamp > 300 # 5 minutes
head :unauthorized
return
end
Testing Your Endpoint
Before relying on webhooks in production, verify your integration works:
- Navigate to the endpoint's detail page
- Click Send Test
- Breakwater sends a
test.pingevent to your URL - Check the delivery log on the same page to confirm it succeeded
The test ping endpoint must be enabled to send a test. If it's disabled, re-enable it first.
Delivery Log
Each endpoint has a delivery log showing recent deliveries with:
- Event type: What triggered the delivery
- Status: Whether it succeeded, failed, or is being retried
- HTTP response code: The status code your server returned
- Timestamp: When the delivery was attempted
Use the delivery log to debug integration issues. Delivery logs are retained for 30 days.
Retries
If your endpoint returns a non-2xx response or is unreachable, Breakwater retries the delivery with exponential backoff:
| Attempt | Retry after |
|---|---|
| 1 | 30 seconds |
| 2 | 2 minutes |
| 3 | 15 minutes |
| 4 | 1 hour |
| 5 | 4 hours |
| 6 | 24 hours |
After 6 failed attempts, the delivery is marked as exhausted.
Auto-Disable
If 3 separate events each exhaust all retry attempts, the endpoint is automatically disabled and you'll receive an email notification. This prevents Breakwater from continuing to send to a broken endpoint.
To recover:
- Fix the issue with your receiving server
- Navigate to the endpoint in the vendor portal
- Click Send Test to verify it's working (re-enable first if needed)
- Re-enable the endpoint
The failure counter resets when you re-enable an endpoint.
Managing Endpoints
Editing
- Click on the endpoint to view details
- Click Edit
- Update the URL, name, description, enabled status, or event subscriptions
- Click Update Webhook Endpoint
Disabling
You can disable an endpoint without deleting it — useful during maintenance:
- Edit the endpoint
- Uncheck Enabled
- Save
While disabled, no events are delivered. Events that occur while the endpoint is disabled are not queued or delivered later.
Deleting
- Click on the endpoint to view details
- Click Delete
- Confirm the deletion
Deleting an endpoint removes it and all its delivery history.
Regenerating the Signing Secret
If your signing secret is compromised:
- Delete the endpoint
- Create a new endpoint with the same URL and event subscriptions
- Update your receiving application with the new signing secret
Best Practices
Security
- Always verify signatures — Don't trust webhook payloads without checking the HMAC signature
- Use HTTPS — Always use HTTPS URLs to protect webhook payloads in transit
- Check timestamps — Reject requests with old timestamps to prevent replay attacks
- Store secrets securely — Keep your signing secret in environment variables, not in source code
Reliability
- Respond quickly — Return a 2xx response within 10 seconds. Process the event asynchronously if it takes longer
- Handle duplicates — In rare cases, the same event may be delivered more than once. Use the event
idto deduplicate - Monitor the delivery log — Check periodically to catch issues before the endpoint gets auto-disabled
Architecture
- One endpoint per system — Route different events to different endpoints rather than building a single dispatcher
- Subscribe only to what you need — Fewer subscriptions means less noise and less processing
- Use the
typefield — Always check the event type before processing, even if you only subscribe to one type