Webhooks
Webhooks let Creatorlayer push events to your server the moment a verification changes state — eliminating the need to poll.
Register an endpoint
curl -X POST https://api.creatorlayer.eu/api/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/creatorlayer",
"events": ["verification.completed", "verification.failed"]
}'
Response:
{
"webhook_id": "whk_01JXXXXXXXXXXXXXXXXXXXXXXX",
"url": "https://your-app.example.com/webhooks/creatorlayer",
"events": ["verification.completed", "verification.failed"],
"created_at": "2026-03-19T12:00:00Z"
}
Store webhook_id if you need to update or delete the endpoint later.
Verify the signature
Every webhook request includes an X-Creatorlayer-Signature header — a HMAC-SHA256 of the raw request body signed with your webhook secret. Always verify this before processing the payload.
import hmac, hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(body: Buffer, signature: string, secret: string): boolean {
const expected = createHmac("sha256", secret).update(body).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Always use a constant-time comparison (hmac.compare_digest / timingSafeEqual) to prevent timing attacks. Never compare signatures with ==.
Event catalogue
verification.completed
Fired when a creator has consented and the Risk Tape is ready to retrieve.
{
"event": "verification.completed",
"verification_id": "vrf_01JXXXXXXXXXXXXXXXXXXXXXXX",
"obligor_reference": "creator-abc-123",
"occurred_at": "2026-03-19T14:32:00Z"
}
Next step: fetch the full tape at GET /api/v1/verifications/:id/tape.
verification.failed
Fired when data collection failed — for example, the creator revoked OAuth access mid-flow or a platform returned an error.
{
"event": "verification.failed",
"verification_id": "vrf_01JXXXXXXXXXXXXXXXXXXXXXXX",
"obligor_reference": "creator-abc-123",
"reason": "platform_error",
"occurred_at": "2026-03-19T14:35:00Z"
}
reason values:
| Value | Meaning |
|---|---|
platform_error | A connected platform (YouTube, Stripe) returned an error |
consent_revoked | The creator revoked OAuth access before data was fetched |
timeout | Data collection did not complete within the processing window |
Next step: create a new verification and send the creator a fresh consent_url.
verification.expired
Fired when a pending_consent verification reaches its 7-day expiry without the creator completing the consent flow.
{
"event": "verification.expired",
"verification_id": "vrf_01JXXXXXXXXXXXXXXXXXXXXXXX",
"obligor_reference": "creator-abc-123",
"occurred_at": "2026-03-26T12:00:00Z"
}
Next step: create a new verification if you still need income data for this creator.
Delivery and retries
| Property | Value |
|---|---|
| Delivery method | HTTPS POST |
| Timeout | 10 seconds |
| Retry schedule | Immediate → 1 min → 5 min → 30 min → 2 hr (5 attempts total) |
| Retry trigger | Non-2xx response or connection timeout |
| Ordering | Events are delivered in order per verification_id but not across verifications |
Return any 2xx status to acknowledge receipt. If your endpoint returns a non-2xx status or times out, Creatorlayer will retry using the schedule above.
If your handler is slow, respond with 200 OK immediately and process the payload asynchronously.
Manage endpoints
List webhooks
curl https://api.creatorlayer.eu/api/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY"
Delete a webhook
curl -X DELETE https://api.creatorlayer.eu/api/v1/webhooks/whk_01JXXXXXXXXXXXXXXXXXXXXXXX \
-H "Authorization: Bearer YOUR_API_KEY"
Testing in sandbox
In the sandbox (api-sandbox.creatorlayer.eu), use an obligor_reference starting with test- to trigger synthetic events without a real creator consent flow:
test-complete-*— triggersverification.completedwithin ~5 secondstest-fail-*— triggersverification.failed(reason:platform_error)test-expire-*— triggersverification.expired
curl -X POST https://api-sandbox.creatorlayer.eu/api/v1/verifications \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"obligor_reference": "test-complete-001",
"creator_platforms": ["youtube"],
"lender_name": "Acme Finance",
"product_type": "rbf"
}'
Your registered webhook will receive the event within seconds.
Signature Verification
Every webhook request from Creatorlayer includes an X-Creatorlayer-Signature header. Always verify this header before processing the event. An unverified webhook endpoint is a security risk — anyone who knows your URL could send forged events.
How it works
Creatorlayer signs the raw request body with HMAC-SHA256 using your endpoint's secret (visible in the dashboard). The signature is prefixed with sha256= so you can distinguish the algorithm version in future upgrades.
X-Creatorlayer-Signature: sha256=3d1b2e4f...
Critical: always compute the signature over the raw, unparsed request body — not over a JSON re-serialisation of it. Even whitespace differences will cause a mismatch.
Headers reference
| Header | Description |
|---|---|
X-Creatorlayer-Signature | HMAC-SHA256 signature of the raw body: sha256=<lowercase-hex> |
X-Creatorlayer-Event | Event type, e.g. verification.completed, tape.updated |
X-Creatorlayer-Delivery | Unique delivery UUID — use this for idempotency |
Node.js verification
const crypto = require('crypto');
/**
* Verify a Creatorlayer webhook signature.
* @param {string|Buffer} payload - Raw request body (do NOT use parsed JSON)
* @param {string} signature - Value of X-Creatorlayer-Signature header
* @param {string} secret - Your endpoint secret from the dashboard
* @returns {boolean}
*/
function verifySignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(typeof payload === 'string' ? payload : payload.toString('utf8'))
.digest('hex');
if (typeof signature !== 'string' || !signature.startsWith('sha256=')) {
throw new Error('Invalid or missing signature header');
}
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express endpoint example — note express.raw() to preserve the raw body
app.post(
'/webhooks/creatorlayer',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-creatorlayer-signature'];
try {
verifySignature(req.body, signature, process.env.CREATORLAYER_WEBHOOK_SECRET);
} catch {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
const deliveryId = req.headers['x-creatorlayer-delivery'];
// Process event asynchronously — respond quickly
processWebhookAsync(event, deliveryId).catch(console.error);
res.sendStatus(200);
}
);
Python verification
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
"""
Verify a Creatorlayer webhook signature.
Args:
payload: Raw request body as bytes (do NOT use parsed JSON)
signature: Value of X-Creatorlayer-Signature header
secret: Your endpoint secret from the dashboard
Returns:
True if valid. Raises ValueError on format errors.
"""
if not signature or not signature.startswith('sha256='):
raise ValueError(f'Invalid or missing signature header: {signature!r}')
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask endpoint example
from flask import Flask, request, abort
import json
app = Flask(__name__)
@app.route('/webhooks/creatorlayer', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Creatorlayer-Signature', '')
secret = os.environ['CREATORLAYER_WEBHOOK_SECRET']
if not verify_signature(request.get_data(), signature, secret):
abort(401)
event = request.get_json()
delivery_id = request.headers.get('X-Creatorlayer-Delivery')
# Process asynchronously — respond quickly
process_webhook.delay(event, delivery_id)
return '', 200
Retry policy
If your endpoint does not return a 2xx status within 30 seconds, Creatorlayer marks the delivery as failed and schedules a retry.
| Attempt | Delay after previous failure |
|---|---|
| 1st (initial) | Immediate |
| 2nd | 1 minute |
| 3rd | 5 minutes |
| 4th | 30 minutes |
After 4 failed attempts, the delivery is marked failed and no further retries are made. You can manually replay deliveries from the dashboard.
Best practices
- Verify before processing. Reject any request where signature verification fails with
401. - Use the raw body. Parse JSON only after verification. Many frameworks buffer the body — ensure you are reading the pre-parse bytes.
- Respond quickly, process asynchronously. Your endpoint must respond within 30 seconds. Offload heavy work to a queue or background job.
- Use
X-Creatorlayer-Deliveryfor idempotency. Store delivery UUIDs you have successfully processed to safely handle retries without double-processing. - Rotate secrets periodically. Generate a new secret from the dashboard and update your environment variable. A brief dual-verification window (accepting both old and new) eases zero-downtime rotation.