Webhooks
Webhooks allow you to receive real-time notifications when events occur in your LabelGrid account. Use webhooks to automate workflows and integrate with external systems.
For Developers: You can also manage webhooks programmatically via the API. See the LabelGrid API Documentation for endpoints and examples.
How Webhooks Work
Section titled “How Webhooks Work”- You configure a webhook - Specify a URL and which events to listen for
- An event occurs - For example, a release is delivered to a store
- LabelGrid sends a POST request - Your server receives the event data
- Your system processes it - Automate workflows based on the event
Accessing Webhooks
Section titled “Accessing Webhooks”- Click your profile icon in the top-right corner
- Select Webhooks from the dropdown menu
Creating a Webhook
Section titled “Creating a Webhook”- Click Create Webhook
- Enter a Name to identify this webhook
- Enter the URL where you want to receive notifications
- Select which Events should trigger this webhook
- Click Create
Webhook Secret
Section titled “Webhook Secret”When you create a webhook, you’ll receive a secret key. Use this to verify that incoming requests are actually from LabelGrid:
- Store the secret securely
- Verify the signature on incoming requests
- If compromised, regenerate the secret
Available Events
Section titled “Available Events”Configure your webhook to listen for these events. The Event Identifier is the value you’ll see in the event property of the payload and in the X-Webhook-Event header:
| Event Identifier | Description |
|---|---|
delivery.completed | Triggered when a release is successfully delivered to an outlet |
delivery.failed | Triggered when delivery to an outlet fails |
takedown.completed | Triggered when a takedown request completes |
release.review.status_changed | Triggered when a release review status changes |
release.distributed | Triggered when a release is distributed |
payment.statement_ready | Triggered when a payment statement is ready for viewing |
You can select multiple events for a single webhook, or create separate webhooks for different event types.
Managing Webhooks
Section titled “Managing Webhooks”Viewing Your Webhooks
Section titled “Viewing Your Webhooks”The webhook list shows:
| Column | Description |
|---|---|
| Name | The webhook name you assigned |
| URL | Where notifications are sent |
| Events | Number of events configured |
| Status | Active or Inactive |
| Success / Fail | Count of successful and failed deliveries |
| Last Triggered | When the webhook was last called |
Editing a Webhook
Section titled “Editing a Webhook”- Click the Edit action on the webhook row
- Modify the name, URL, or events
- Click Save
Activating / Deactivating
Section titled “Activating / Deactivating”Toggle a webhook’s active status without deleting it:
- Active - Webhook will receive notifications
- Inactive - Webhook is paused, no notifications sent
Deleting a Webhook
Section titled “Deleting a Webhook”- Click the Delete action on the webhook row
- Confirm the deletion
Testing Webhooks
Section titled “Testing Webhooks”Before relying on a webhook in production, test it:
- Click the Test action on your webhook
- LabelGrid sends a test payload to your URL
- Check that your endpoint received and processed it correctly
Viewing Webhook Logs
Section titled “Viewing Webhook Logs”Monitor webhook activity and troubleshoot issues:
- Click the View Logs action on a webhook
- See a history of all webhook deliveries
Log Details
Section titled “Log Details”Each log entry shows:
| Field | Description |
|---|---|
| Event Type | Which event triggered this delivery |
| Response Status | HTTP status code from your server |
| Duration | How long the request took |
| Attempt | Retry attempt number |
| Timestamp | When the delivery occurred |
Webhook Payload Format
Section titled “Webhook Payload Format”When an event occurs, LabelGrid sends a POST request to your URL with a JSON payload:
{ "event": "delivery.completed", "timestamp": "2026-05-05T10:00:00+00:00", "webhook_id": "123", "data": { // Event-specific data }}The timestamp field uses ISO 8601 format. webhook_id is the ID of your configured webhook (it matches the X-Webhook-Id header).
Event Payloads
Section titled “Event Payloads”The data object structure depends on the event type. All field types below are JSON types as serialized in the payload.
delivery.completed
Section titled “delivery.completed”Fired once per outlet when a release delivery reaches a terminal success state.
{ "event": "delivery.completed", "timestamp": "2026-05-18T10:00:00+00:00", "webhook_id": "123", "data": { "distro_queue_id": 456, "release_id": 789, "release_cat": "ABC123", "outlet_id": 12, "outlet_name": "Spotify", "status": "complete" }}| Field | Type | Description |
|---|---|---|
distro_queue_id | integer | Internal queue ID for this delivery attempt |
release_id | integer | The release that was delivered |
release_cat | string | null | Your release catalog reference |
outlet_id | integer | The destination outlet ID |
outlet_name | string | null | Human-readable outlet name (e.g. "Spotify") |
status | string | Always "complete" for this event |
delivery.failed
Section titled “delivery.failed”Fired once per outlet when a release delivery reaches a terminal failure state. Same payload as delivery.completed plus a message field.
{ "event": "delivery.failed", "timestamp": "2026-05-18T10:00:00+00:00", "webhook_id": "123", "data": { "distro_queue_id": 456, "release_id": 789, "release_cat": "ABC123", "outlet_id": 12, "outlet_name": "Spotify", "status": "error", "message": "Outlet rejected the delivery: missing ISRC." }}| Field | Type | Description |
|---|---|---|
status | string | One of error, fault, rejected, batch_exception |
message | string | null | Failure reason from the outlet or distribution pipeline |
takedown.completed
Section titled “takedown.completed”Fired once per outlet when a takedown request succeeds. Same shape as delivery.completed plus a takedown: true flag.
{ "event": "takedown.completed", "timestamp": "2026-05-18T10:00:00+00:00", "webhook_id": "123", "data": { "distro_queue_id": 456, "release_id": 789, "release_cat": "ABC123", "outlet_id": 12, "outlet_name": "Spotify", "status": "complete", "takedown": true }}release.distributed
Section titled “release.distributed”Fired once per release when the release transitions to the distributed delivery state. Only fires on the transition into distributed — not on subsequent saves while the release is already distributed.
{ "event": "release.distributed", "timestamp": "2026-05-18T10:00:00+00:00", "webhook_id": "123", "data": { "release_id": 789, "release_cat": "ABC123", "release_title": "Summer EP", "delivery_status": "distributed" }}release.review.status_changed
Section titled “release.review.status_changed”Fired whenever a release moves between review states.
{ "event": "release.review.status_changed", "timestamp": "2026-05-18T10:00:00+00:00", "webhook_id": "123", "data": { "release_id": 789, "release_cat": "ABC123", "release_title": "Summer EP", "previous_status": "to_review", "new_status": "approved" }}| Field | Type | Description |
|---|---|---|
previous_status | string | Prior status. One of draft, to_review, approved, rejected, require_changes, audit |
new_status | string | New status. Same set of values |
payment.statement_ready
Section titled “payment.statement_ready”Fired when a payment statement is generated and ready for viewing.
{ "event": "payment.statement_ready", "timestamp": "2026-05-18T10:00:00+00:00", "webhook_id": "123", "data": { "payment_request_id": 1024, "invoice_number": "INV-2026-001", "period": "2026-04-30", "amount": 1234.56, "total_due_usd": 1234.56, "currency": "USD" }}| Field | Type | Description |
|---|---|---|
payment_request_id | integer | Internal payment request ID |
invoice_number | string | Invoice reference for the statement |
period | string | null | End-of-period date (ISO 8601 date, YYYY-MM-DD) |
amount | number | Statement amount in currency |
total_due_usd | number | Statement total converted to USD |
currency | string | ISO 4217 currency code (defaults to USD) |
Verifying Webhook Signatures
Section titled “Verifying Webhook Signatures”Every webhook delivery is signed so you can verify it actually came from LabelGrid. Always verify the signature before processing the event.
Request Headers
Section titled “Request Headers”Every webhook POST request includes these headers:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 of the raw request body, lowercase hex, no algorithm prefix |
X-Webhook-Timestamp | ISO 8601 timestamp of the delivery (same value as the timestamp property in the body) |
X-Webhook-Event | Event identifier (e.g., delivery.completed) |
X-Webhook-Id | The ID of the webhook receiving the delivery |
User-Agent | LabelGrid-Webhooks/1.0 |
Content-Type | application/json |
Algorithm
Section titled “Algorithm”- Algorithm: HMAC-SHA256
- Encoding: Lowercase hexadecimal
- Prefix: None — the value is just the hex digest, not
sha256=... - Signed content: The full raw JSON request body (the body itself includes the timestamp as a property, so the timestamp is signed implicitly)
Verification Recipe
Section titled “Verification Recipe”- Read the raw request body before any JSON parsing or transformation. Re-serializing the parsed JSON may produce different bytes and break the signature.
- Compute
HMAC-SHA256(raw_body, your_webhook_secret)and take the lowercase hex digest. - Compare against
X-Webhook-Signatureusing a constant-time comparison. - (Recommended) Reject the request if
X-Webhook-Timestampis older than your replay tolerance window — we suggest 5 minutes.
PHP Example
Section titled “PHP Example”$rawBody = file_get_contents('php://input');$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$expected = hash_hmac('sha256', $rawBody, $webhookSecret);
if (! hash_equals($expected, $signature)) { http_response_code(401); exit('Invalid signature');}
// Reject deliveries older than 5 minutesif (abs(time() - strtotime($timestamp)) > 300) { http_response_code(401); exit('Stale delivery');}
$payload = json_decode($rawBody, true);// ... process the eventhttp_response_code(200);Node.js Example
Section titled “Node.js Example”const crypto = require('crypto');
// Express: capture raw body BEFORE any JSON middlewareapp.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body; // Buffer const signature = req.header('X-Webhook-Signature') || ''; const timestamp = req.header('X-Webhook-Timestamp') || '';
const expected = crypto .createHmac('sha256', webhookSecret) .update(rawBody) .digest('hex');
const sigBuf = Buffer.from(signature, 'hex'); const expBuf = Buffer.from(expected, 'hex');
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) { return res.status(401).send('Invalid signature'); }
if (Math.abs(Date.now() - new Date(timestamp).getTime()) > 5 * 60 * 1000) { return res.status(401).send('Stale delivery'); }
const payload = JSON.parse(rawBody.toString('utf8')); // ... process the event res.sendStatus(200);});Python Example
Section titled “Python Example”import hmac, hashlibfrom datetime import datetime, timezone
raw_body = request.get_data() # Flask: bytes, before any JSON parsingsignature = request.headers.get('X-Webhook-Signature', '')timestamp = request.headers.get('X-Webhook-Timestamp', '')
expected = hmac.new( webhook_secret.encode('utf-8'), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature): return ('Invalid signature', 401)
delivery_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))if abs((datetime.now(timezone.utc) - delivery_time).total_seconds()) > 300: return ('Stale delivery', 401)
return ('', 200) # process the event, then ackCommon Pitfalls
Section titled “Common Pitfalls”- Re-serializing the body before hashing. Frameworks that auto-parse JSON (Express
express.json(), Laravel default request body) lose the original bytes. Capture the raw body first. - Using a non-constant-time comparison (
==,===). Susceptible to timing attacks — always usehash_equals(PHP),crypto.timingSafeEqual(Node),hmac.compare_digest(Python), or your language’s equivalent. - Expecting a
sha256=prefix. The header value is just the hex digest with no prefix. - Skipping the timestamp check. Without it, a leaked signature could be replayed indefinitely.
Limits and Reliability
Section titled “Limits and Reliability”Request Limits
Section titled “Request Limits”| Limit | Value |
|---|---|
| Request timeout | 10 seconds |
| Maximum payload size | 64 KB |
| Maximum webhooks per user | 10 |
If your endpoint does not respond within 10 seconds, the delivery is treated as a failure and retried.
Retry Schedule
Section titled “Retry Schedule”If your endpoint returns a non-2xx status or times out, LabelGrid retries with exponential backoff:
| Attempt | Wait before retry |
|---|---|
| 1 → 2 | 30 seconds |
| 2 → 3 | 1 minute |
| 3 → 4 | 2 minutes |
| 4 → 5 | 4 minutes |
| 5 → 6 | 8 minutes |
| 6 → 7 | 16 minutes |
| 7 → 8 | 32 minutes |
| 8 → 9 | 64 minutes |
| 9 → 10 | 128 minutes |
Each interval includes 0–30 seconds of jitter. After 10 attempts (~4.5 hours total elapsed), the delivery is logged as permanently failed and does not retry further.
Automatic Disablement
Section titled “Automatic Disablement”If a webhook accumulates 100 cumulative failed deliveries, it is automatically disabled. Re-enable it manually in the webhook list after fixing your endpoint. Reactivating a disabled webhook resets its failure count.
Best Practices for Reliability
Section titled “Best Practices for Reliability”- Return a 2xx response quickly (within 10 seconds)
- Process the data asynchronously after acknowledging
- Verify the signature on every request (see Verifying Webhook Signatures)
- Monitor your failure count in the webhook list
- Check delivery logs when investigating missed events
Use Cases
Section titled “Use Cases”Automated Notifications
Section titled “Automated Notifications”- Send Slack messages when releases go live
- Email your team when deliveries fail
- Update internal dashboards
Workflow Automation
Section titled “Workflow Automation”- Trigger marketing campaigns when releases are distributed
- Update your website when new content is available
- Sync status to external project management tools
Monitoring and Alerting
Section titled “Monitoring and Alerting”- Get instant alerts for delivery failures
- Track distribution progress in real-time
- Monitor review status changes
Troubleshooting
Section titled “Troubleshooting”Webhook Not Receiving Events
Section titled “Webhook Not Receiving Events”- Check status - Is the webhook Active?
- Verify URL - Is the endpoint accessible from the internet?
- Check events - Are the right events selected?
- Review logs - Any errors recorded?
High Failure Count
Section titled “High Failure Count”- Check your endpoint - Is it returning 200 OK?
- Check response time - Is it responding within timeout?
- Review error messages - What’s failing?
- Test manually - Send a test webhook
Regenerating the Secret
Section titled “Regenerating the Secret”If your webhook secret is compromised:
- Click Regenerate Secret in webhook settings
- Update your application with the new secret
- Old secret immediately stops working
Need Help?
Section titled “Need Help?”If you have questions about webhooks, contact our support team.
Not using LabelGrid yet?
Everything you just read about is available on our platform.
See what LabelGrid can do →