Webhooks
Receive real-time notifications when events happen on the TimeBack Platform. Instead of polling APIs, register a webhook subscription and the platform will POST a signed JSON payload to your HTTPS endpoint within seconds of the event.
Quick Start
- Register your App — see Level 0: Register Your App. You receive your App ID and OAuth credentials in the response.
- Authenticate — exchange your credentials for an access token using the OAuth client credentials flow.
- Create a webhook subscription —
POST /webhooks/1.0/{sourcedApplicationId}/subscriptionswith your consumer URL and the event types you want. - Store the signing secret — the response includes a
signingSecret(prefixedwhsec_). This is shown exactly once. Store it in your secrets manager immediately. - Implement your consumer endpoint — an HTTPS endpoint that verifies the signature and returns
2xxwithin 10 seconds. - Go live — the platform starts delivering matching events to your endpoint within seconds of occurrence.
Prerequisites
- A registered App with an App ID (see Level 0: Register Your App)
- OAuth credentials (
clientId+clientSecret) from registration (see Authentication) - An HTTPS endpoint reachable from the public internet (HTTP is rejected, raw IP addresses are rejected — use a domain name)
Concepts
Subscriptions
A subscription tells the platform where to deliver events and which event types to include. Each subscription belongs to one App and has:
| Field | Type | Description |
|---|---|---|
id |
UUID | Platform-assigned subscription identifier. |
url |
string | Your HTTPS consumer endpoint. Must use https:// with a domain name (raw IP addresses are not allowed). |
eventTypes |
string[] | Event types to subscribe to (e.g., ["insight.created"]). Only events matching these types are delivered. |
status |
enum | active — receiving deliveries. paused — events are queued but held until you resume. disabled — terminal; set by the platform after repeated delivery failures. |
dateCreated |
ISO 8601 | When the subscription was created. |
Event Types
Each event type is a dot-separated string describing what happened (e.g., insight.created, user_profile.updated). Event types are registered by platform modules as they ship — your subscription only receives events matching the types you listed.
Available event types are listed in the Event Types Reference section below. The engine is live; production event types will be added incrementally as platform modules integrate with it.
Thin Payloads
Webhook payloads contain entity references only, not full entity data. This keeps payloads small, avoids stale-data issues, and respects access control — your consumer fetches the current state from the standard REST APIs using its own credentials and scopes.
For example, an insight.created event would include the insight ID and student ID, but not the insight text or student profile. Your consumer calls GET /insights/... with its own token to retrieve the full data.
Authentication for Webhook Management
Webhook management endpoints use the same OAuth 2.0 authentication as all other TimeBack Platform APIs. You need a valid access token obtained via the client credentials flow.
The webhook management endpoints are scoped by App ownership — your token must belong to the same App (auth client) that owns the subscription. In addition, the calling user must hold the app:manage_webhooks permission grant on urn:app:{sourcedApplicationId}. This grant is issued automatically to the App owner at registration time.
Note: Webhook management (creating/updating subscriptions) uses OAuth tokens. Webhook deliveries (the POST requests to your endpoint) are authenticated differently — via HMAC-SHA256 signatures, not OAuth. See Verifying Signatures below.
Subscription Management
Create a Subscription
POST /webhooks/1.0/{sourcedApplicationId}/subscriptions
Request body:
{
"url": "https://your-service.example.com/webhooks",
"eventTypes": ["insight.created"]
}
Response (201):
{
"id": "eee11b2d-66db-4ed0-85c4-3438eccf733f",
"url": "https://your-service.example.com/webhooks",
"eventTypes": ["insight.created"],
"status": "active",
"dateCreated": "2026-06-13T08:37:30.542Z",
"signingSecret": "whsec_123a4ae3aa67fc9ed203aef225f8ee9abc28f43b..."
}
Important: The
signingSecretis only returned at creation time and when you explicitly rotate it. It is never included in list or get responses. Store it immediately — if you lose it, rotate via the rotate-secret endpoint.
Validation rules:
urlmust be a valid HTTPS URL with a domain name (HTTP is rejected with400, raw IP addresses likehttps://1.2.3.4/hookare rejected with400).eventTypesmust contain at least one entry, and every entry must be a recognized event type (see Event Types Reference). Unknown types are rejected with400.
List Subscriptions
GET /webhooks/1.0/{sourcedApplicationId}/subscriptions
Response (200):
[
{
"id": "eee11b2d-66db-4ed0-85c4-3438eccf733f",
"url": "https://your-service.example.com/webhooks",
"eventTypes": ["insight.created"],
"status": "active",
"dateCreated": "2026-06-13T08:37:30.542Z"
}
]
Returns all subscriptions for the App. The signing secret is never included.
Get a Subscription
GET /webhooks/1.0/{sourcedApplicationId}/subscriptions/{subscriptionId}
Returns a single subscription. The signing secret is never included.
Update a Subscription
PUT /webhooks/1.0/{sourcedApplicationId}/subscriptions/{subscriptionId}
Request body (all fields optional — include only what you want to change):
{
"url": "https://new-endpoint.example.com/webhooks",
"eventTypes": ["insight.created", "user_profile.updated"],
"status": "paused"
}
Status transitions:
| From | To | Effect |
|---|---|---|
active |
paused |
Delivery attempts stop, but the platform continues recording events for this subscription. No SQS messages are enqueued while paused — events queue silently in the DB. |
paused |
active |
All events that accumulated during the pause are flushed and delivered. No manual catch-up needed. |
disabled |
active |
Re-enables a subscription that the platform auto-disabled. Events that occurred while disabled are not retroactively delivered — catch up via the REST APIs. |
You cannot set status to disabled — that state is reserved for platform auto-disable after repeated delivery failures.
Paused vs Disabled:
pausedis a lossless pause — events accumulate and are delivered when you resume.disabledis a hard stop — events that occur while disabled are dropped. Usepausedfor planned maintenance windows;disabledis set automatically by the platform after terminal delivery failures.
Delete a Subscription
DELETE /webhooks/1.0/{sourcedApplicationId}/subscriptions/{subscriptionId}
Response: 204 No Content
Soft-deletes the subscription. No further deliveries are attempted. This action is not reversible — create a new subscription if you need to resume.
Rotate the Signing Secret
POST /webhooks/1.0/{sourcedApplicationId}/subscriptions/{subscriptionId}/rotate-secret
Response (200):
{
"id": "eee11b2d-66db-4ed0-85c4-3438eccf733f",
"url": "https://your-service.example.com/webhooks",
"eventTypes": ["insight.created"],
"status": "active",
"dateCreated": "2026-06-13T08:37:30.542Z",
"signingSecret": "whsec_newrotatedsecret..."
}
Generates a new signing secret and returns it. During the rotation window, both the old and new secrets are valid — in-flight deliveries signed with the previous secret still verify. Update your consumer to use the new secret as soon as possible.
When to rotate:
- Your secret may have been exposed (e.g., committed to a public repo).
- Your security policy requires periodic rotation.
- You lost the original secret and need a new one.
Delivery Format
When an event matches your subscription, the platform sends an HTTPS POST to your url with the following:
Headers
| Header | Value | Description |
|---|---|---|
Content-Type |
application/json |
Always JSON. |
X-TimeBack-Webhook-Timestamp |
Unix seconds (e.g., 1718267529) |
When the delivery was signed. Use for replay protection. |
X-TimeBack-Webhook-Signature |
Hex string (64 chars) | HMAC-SHA256 signature over the timestamp and body. |
Body (Envelope)
{
"id": "fec49ed7-2130-493b-95d6-089e91ffd92e",
"type": "insight.created",
"timestamp": "2026-06-13T08:42:09.204Z",
"data": {
"insightId": "abc123",
"studentId": "def456"
}
}
| Field | Type | Description |
|---|---|---|
id |
UUID | Stable event identifier. The same id is sent to all matching subscriptions and across retries. Use it to deduplicate on your side. |
type |
string | The event type string (matches one of your subscribed eventTypes). |
timestamp |
ISO 8601 | When the event originally occurred on the platform. |
data |
object | Entity references specific to the event type. Contains IDs and URNs only — never full entity data. The shape of data varies by event type; see Event Types Reference. |
Verifying Signatures
Every delivery is signed with your subscription's signingSecret using HMAC-SHA256. Always verify signatures before processing a delivery — this confirms the request came from TimeBack and hasn't been tampered with.
Algorithm
- Extract the
X-TimeBack-Webhook-Timestampheader value. - Read the raw request body as a UTF-8 string (before JSON parsing).
- Concatenate:
{timestamp}.{body}(the timestamp, a literal dot, then the raw body). - Compute
HMAC-SHA256using yoursigningSecretas the key and the concatenated string as the message. - Hex-encode the result.
- Compare the computed hex string against the
X-TimeBack-Webhook-Signatureheader using a constant-time comparison function to prevent timing attacks. - (Recommended) Reject deliveries whose timestamp is more than 5 minutes old (
300seconds) to prevent replay attacks.
Node.js Example
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhookSignature(secret, timestamp, body, signature) {
const payload = `${timestamp}.${body}`;
const expected = createHmac('sha256', secret).update(payload).digest('hex');
const expectedBuf = Buffer.from(expected, 'utf-8');
const signatureBuf = Buffer.from(signature, 'utf-8');
if (expectedBuf.length !== signatureBuf.length) return false;
return timingSafeEqual(expectedBuf, signatureBuf);
}
// Express.js handler example:
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.headers['x-timeback-webhook-timestamp'];
const signature = req.headers['x-timeback-webhook-signature'];
const body = req.body.toString('utf-8');
// 1. Verify signature
if (!verifyWebhookSignature(process.env.WEBHOOK_SECRET, timestamp, body, signature)) {
return res.status(401).send('Invalid signature');
}
// 2. Reject stale deliveries (replay protection)
const ageSeconds = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (ageSeconds > 300) {
return res.status(401).send('Timestamp too old');
}
// 3. Parse and process
const event = JSON.parse(body);
console.log(`Received ${event.type} event: ${event.id}`);
// 4. Deduplicate — check if you've already processed this event.id
// ...your deduplication logic here...
// 5. Respond 200 immediately, process asynchronously if needed
res.status(200).send('OK');
});
Gotcha: Use
express.raw()(notexpress.json()) to get the raw body bytes. If Express parses JSON first, the re-serialized body may differ from the original (key order, whitespace) and the signature won't match.
Python (Flask) Example
import hashlib
import hmac
import json
import time
from flask import Flask, request
app = Flask(__name__)
def verify_webhook(secret: str, timestamp: str, body: str, signature: str) -> bool:
payload = f"{timestamp}.{body}"
expected = hmac.new(
secret.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
timestamp = request.headers.get('X-TimeBack-Webhook-Timestamp', '')
signature = request.headers.get('X-TimeBack-Webhook-Signature', '')
body = request.get_data(as_text=True)
if not verify_webhook(WEBHOOK_SECRET, timestamp, body, signature):
return 'Invalid signature', 401
if time.time() - int(timestamp) > 300:
return 'Timestamp too old', 401
event = json.loads(body)
print(f"Received {event['type']} event: {event['id']}")
# Deduplicate by event['id'] and process...
return 'OK', 200
Consumer Requirements
Your webhook consumer endpoint must satisfy all of the following:
| Requirement | Details |
|---|---|
| HTTPS only | HTTP URLs are rejected at subscription creation time. Self-signed certificates are not supported in production. |
| Respond within 10 seconds | The platform times out after 10 seconds. If your processing takes longer, return 200 immediately and process asynchronously (e.g., push to your own queue). |
Return 2xx on success |
Any 2xx status code (200, 201, 202, 204) marks the delivery as successful. |
| Be idempotent | The same event id may be delivered more than once (at-least-once delivery semantics). Always check the id field against your processed-events store before acting. |
| Verify signatures | Reject any request with an invalid or missing X-TimeBack-Webhook-Signature. See Verifying Signatures. |
| Publicly reachable | Your endpoint must be reachable from AWS us-east-1. Localhost URLs and private IPs will not work. |
Retry & Failure Behavior
| Your endpoint returns | Platform behavior |
|---|---|
2xx (200, 201, etc.) |
Delivery marked as delivered. Done. |
429 (Too Many Requests) |
Delivery stays pending and is retried — the platform respects your rate limit signal. |
408 (Request Timeout) |
Delivery stays pending and is retried — treated as a transient timeout. |
4xx (other: 400, 401, 403, etc.) |
Delivery marked as dead (terminal, no retry). The platform treats other 4xx as a permanent rejection. |
5xx (500, 502, 503, etc.) |
Delivery stays pending and is retried. Up to ~96 retry attempts over approximately 24 hours, with increasing backoff. |
| Timeout (no response in 10s) | Same as 5xx — retry with backoff. |
| Connection refused / DNS failure | Same as 5xx — retry with backoff. |
| All retries exhausted | Delivery marked as dead. The subscription is auto-disabled (status: "disabled") — the endpoint has been unreachable for the full retry window. |
Recovering from Auto-disable
If your subscription is auto-disabled after exhausting retries:
- Diagnose and fix your endpoint (check logs, DNS, TLS certificate, firewall rules).
- Re-enable the subscription:
PUT /webhooks/1.0/{sourcedApplicationId}/subscriptions/{subscriptionId}with{"status": "active"}. - Catch up on missed events. Events that occurred while the subscription was disabled are not retroactively delivered. Use the relevant REST APIs to poll for data you may have missed.
Why 4xx Is Terminal
Unlike 5xx (which indicates a transient server error), 4xx responses indicate a client-side issue — the consumer explicitly rejected the payload. Retrying the same payload to an endpoint that already returned 4xx wastes resources for both sides. Common causes:
401— your signature verification is rejecting valid signatures (check your secret).404— your endpoint URL changed but the subscription wasn't updated.400— your consumer has validation that rejects the payload shape.
If you receive unexpected 4xx rejections, check your consumer logs and update your subscription URL or signing secret as needed.
End-to-End Example
Here's the complete flow from subscription creation to processing a delivery:
Step 1 — Create the subscription (your backend, at setup time):
curl -X POST https://platform.timeback.com/webhooks/1.0/{sourcedApplicationId}/subscriptions \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-service.example.com/webhooks",
"eventTypes": ["insight.created"]
}'
Save signingSecret from the response to your secrets manager.
Step 2 — Platform delivers an event (automatic, when an insight is created):
POST /webhooks HTTP/1.1
Host: your-service.example.com
Content-Type: application/json
X-TimeBack-Webhook-Timestamp: 1718267529
X-TimeBack-Webhook-Signature: a1b2c3d4e5f6...
{
"id": "fec49ed7-2130-493b-95d6-089e91ffd92e",
"type": "insight.created",
"timestamp": "2026-06-13T08:42:09.204Z",
"data": {
"insightId": "abc123",
"studentId": "def456"
}
}
Step 3 — Your consumer processes the delivery:
- Verify the HMAC-SHA256 signature using your stored
signingSecret. - Check the timestamp is within 5 minutes.
- Deduplicate by
id(check your database or cache). - Return
200 OKimmediately. - Asynchronously: fetch the full insight data via
GET /insights/...with your own OAuth token, and act on it.
Troubleshooting
"Unknown webhook event type(s)" when creating a subscription
The eventTypes you specified aren't recognized by the platform. Check the Event Types Reference for the current list. Event types are added incrementally as platform modules integrate with the webhook engine.
Signature verification fails on your consumer
- Ensure you're verifying against the raw request body string, not a re-serialized version. JSON parsers may reorder keys or alter whitespace, which changes the signature.
- Confirm your stored secret matches the one returned at subscription creation (or last rotation). Secrets start with
whsec_. - Check that you're concatenating as
{timestamp}.{body}(with a literal.between them).
Subscription was auto-disabled
Your endpoint failed to return 2xx for approximately 24 hours (96 retry attempts). See Recovering from Auto-disable.
Not receiving deliveries
- Verify the subscription
statusisactive(notpausedordisabled). - Confirm the events being emitted match one of the
eventTypesin your subscription. - Ensure your endpoint is publicly reachable from AWS
us-east-1(test with acurlfrom an EC2 instance or similar). - Check that your endpoint returns
2xx—4xxresponses mark deliveries as terminal without retry.
Lost your signing secret
Rotate the secret: POST /webhooks/1.0/{sourcedApplicationId}/subscriptions/{subscriptionId}/rotate-secret. The new secret is returned in the response. Update your consumer immediately.
Event Types Reference
Event types are added as platform modules integrate with the webhook engine. Each event type documents its data shape so you know what entity references to expect.
| Event Type | Description | data Fields |
Since |
|---|---|---|---|
test.ping |
Synthetic event for verifying your integration end-to-end. | { "message": "..." } |
v1.0 |
Additional event types for insights, user profiles, and other platform domains will be documented here as they ship. Subscribe to specific types — your subscription is not affected when new types are added to the platform.
Related Docs
Level 0: Register Your App
Register your App to get an App ID and OAuth credentials — the prerequisite for creating webhook subscriptions.
Authentication
Exchange your OAuth credentials for access tokens to call the webhook management endpoints.
App Lifecycle
Understand draft vs active tiers and how they affect your App's capabilities, including webhook subscriptions.
API Reference
Interactive API reference with full request/response schemas for all webhook endpoints.
