Skip to content
Support

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.

  1. You configure a webhook - Specify a URL and which events to listen for
  2. An event occurs - For example, a release is delivered to a store
  3. LabelGrid sends a POST request - Your server receives the event data
  4. Your system processes it - Automate workflows based on the event

  1. Click your profile icon in the top-right corner
  2. Select Webhooks from the dropdown menu

  1. Click Create Webhook
  2. Enter a Name to identify this webhook
  3. Enter the URL where you want to receive notifications
  4. Select which Events should trigger this webhook
  5. Click Create

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

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 IdentifierDescription
delivery.completedTriggered when a release is successfully delivered to an outlet
delivery.failedTriggered when delivery to an outlet fails
takedown.completedTriggered when a takedown request completes
release.review.status_changedTriggered when a release review status changes
release.distributedTriggered when a release is distributed
payment.statement_readyTriggered 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.


The webhook list shows:

ColumnDescription
NameThe webhook name you assigned
URLWhere notifications are sent
EventsNumber of events configured
StatusActive or Inactive
Success / FailCount of successful and failed deliveries
Last TriggeredWhen the webhook was last called
  1. Click the Edit action on the webhook row
  2. Modify the name, URL, or events
  3. Click Save

Toggle a webhook’s active status without deleting it:

  • Active - Webhook will receive notifications
  • Inactive - Webhook is paused, no notifications sent
  1. Click the Delete action on the webhook row
  2. Confirm the deletion

Before relying on a webhook in production, test it:

  1. Click the Test action on your webhook
  2. LabelGrid sends a test payload to your URL
  3. Check that your endpoint received and processed it correctly

Monitor webhook activity and troubleshoot issues:

  1. Click the View Logs action on a webhook
  2. See a history of all webhook deliveries

Each log entry shows:

FieldDescription
Event TypeWhich event triggered this delivery
Response StatusHTTP status code from your server
DurationHow long the request took
AttemptRetry attempt number
TimestampWhen the delivery occurred

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).


The data object structure depends on the event type. All field types below are JSON types as serialized in the payload.

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"
}
}
FieldTypeDescription
distro_queue_idintegerInternal queue ID for this delivery attempt
release_idintegerThe release that was delivered
release_catstring | nullYour release catalog reference
outlet_idintegerThe destination outlet ID
outlet_namestring | nullHuman-readable outlet name (e.g. "Spotify")
statusstringAlways "complete" for this event

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."
}
}
FieldTypeDescription
statusstringOne of error, fault, rejected, batch_exception
messagestring | nullFailure reason from the outlet or distribution pipeline

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

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

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"
}
}
FieldTypeDescription
previous_statusstringPrior status. One of draft, to_review, approved, rejected, require_changes, audit
new_statusstringNew status. Same set of values

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"
}
}
FieldTypeDescription
payment_request_idintegerInternal payment request ID
invoice_numberstringInvoice reference for the statement
periodstring | nullEnd-of-period date (ISO 8601 date, YYYY-MM-DD)
amountnumberStatement amount in currency
total_due_usdnumberStatement total converted to USD
currencystringISO 4217 currency code (defaults to USD)

Every webhook delivery is signed so you can verify it actually came from LabelGrid. Always verify the signature before processing the event.

Every webhook POST request includes these headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 of the raw request body, lowercase hex, no algorithm prefix
X-Webhook-TimestampISO 8601 timestamp of the delivery (same value as the timestamp property in the body)
X-Webhook-EventEvent identifier (e.g., delivery.completed)
X-Webhook-IdThe ID of the webhook receiving the delivery
User-AgentLabelGrid-Webhooks/1.0
Content-Typeapplication/json
  • 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)
  1. Read the raw request body before any JSON parsing or transformation. Re-serializing the parsed JSON may produce different bytes and break the signature.
  2. Compute HMAC-SHA256(raw_body, your_webhook_secret) and take the lowercase hex digest.
  3. Compare against X-Webhook-Signature using a constant-time comparison.
  4. (Recommended) Reject the request if X-Webhook-Timestamp is older than your replay tolerance window — we suggest 5 minutes.
$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 minutes
if (abs(time() - strtotime($timestamp)) > 300) {
http_response_code(401);
exit('Stale delivery');
}
$payload = json_decode($rawBody, true);
// ... process the event
http_response_code(200);
const crypto = require('crypto');
// Express: capture raw body BEFORE any JSON middleware
app.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);
});
import hmac, hashlib
from datetime import datetime, timezone
raw_body = request.get_data() # Flask: bytes, before any JSON parsing
signature = 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 ack
  • 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 use hash_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.

LimitValue
Request timeout10 seconds
Maximum payload size64 KB
Maximum webhooks per user10

If your endpoint does not respond within 10 seconds, the delivery is treated as a failure and retried.

If your endpoint returns a non-2xx status or times out, LabelGrid retries with exponential backoff:

AttemptWait before retry
1 → 230 seconds
2 → 31 minute
3 → 42 minutes
4 → 54 minutes
5 → 68 minutes
6 → 716 minutes
7 → 832 minutes
8 → 964 minutes
9 → 10128 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.

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.

  • 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

  • Send Slack messages when releases go live
  • Email your team when deliveries fail
  • Update internal dashboards
  • Trigger marketing campaigns when releases are distributed
  • Update your website when new content is available
  • Sync status to external project management tools
  • Get instant alerts for delivery failures
  • Track distribution progress in real-time
  • Monitor review status changes

  1. Check status - Is the webhook Active?
  2. Verify URL - Is the endpoint accessible from the internet?
  3. Check events - Are the right events selected?
  4. Review logs - Any errors recorded?
  1. Check your endpoint - Is it returning 200 OK?
  2. Check response time - Is it responding within timeout?
  3. Review error messages - What’s failing?
  4. Test manually - Send a test webhook

If your webhook secret is compromised:

  1. Click Regenerate Secret in webhook settings
  2. Update your application with the new secret
  3. Old secret immediately stops working

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 →