Postal Developers
REST API

Webhooks

Signed HTTP callbacks when tracks, composers and shares change.

Attach a URL to an API key and Postal will POST a signed JSON event to it whenever something the key can see changes. One webhook per key.

Set up

In the app, open Tools → Integration, enter the endpoint URL and switch it on. The same setting is available to dashboard sessions at POST /integration/api-keys/{id}/webhook.

Your endpoint must accept POST, respond with any 2xx within a few seconds, and be reachable from the public internet over HTTPS.

Events

EventFires when
track.createdA track is uploaded
track.updatedDetails or tags change
track.audio_profile_completedAnalysis finished: tempo, key, tags and content hash are available
composer.created
composer.updated
shareholder.created
shareholder.updated
shareholder.linked_to_trackA composer gets a share on a track
shareholder.unlinked_from_track
shareholder.track_updatedA share percentage or role changes
publisher.created
publisher.updated
publisher.linked_to_track
publisher.unlinked_from_track
publisher_share.updated
publisher_share.deleted

Payload

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: PostalWebhook/1.0
X-Postal-Event: track.audio_profile_completed
X-Postal-Event-Id: 5f0c1e3a-…
X-Postal-Signature: t=1757232000, v1=3b2d…
{
  "id": "5f0c1e3a-…",
  "type": "track.audio_profile_completed",
  "occurred_at": "2026-09-07T09:20:00.000Z",
  "data": {
    "id": "cm1x…",
    "name": "Night Drive v3.wav",
    "audio_profile": { "bpm": 92, "key": "Am", "…": "…" }
  }
}

data is the affected object at the time of the event. Treat it as a hint and re-fetch from the API if you need the current state.

Verify the signature

Each key has its own webhook secret, created the first time an event is sent.

Getting your secret

The Integration console does not show the webhook secret yet. Contact support with your key id to receive it. Until you have it, treat webhook payloads as untrusted hints and re-fetch from the API.

The signature header is t=<unix seconds>, v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<raw body>").

Node
import crypto from 'node:crypto';

export function verify(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.trim().split('=') as [string, string]),
  );
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Verify against the raw request body, before any JSON parsing or re-serialising.

Delivery and retries

  • Events are queued per key and delivered in the background.
  • A non-2xx response or a network error is retried up to 5 times in total, after 1 minute, 5 minutes, 30 minutes, 2 hours and 24 hours.
  • After the fifth failure the event is marked dead and not retried.
  • Deliveries can arrive out of order and, in rare cases, more than once. Use X-Postal-Event-Id to deduplicate.
  • Switching the webhook off drops any events still pending for that key.

On this page