Skip to main content

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));
}
warning

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:

ValueMeaning
platform_errorA connected platform (YouTube, Stripe) returned an error
consent_revokedThe creator revoked OAuth access before data was fetched
timeoutData 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

PropertyValue
Delivery methodHTTPS POST
Timeout10 seconds
Retry scheduleImmediate → 1 min → 5 min → 30 min → 2 hr (5 attempts total)
Retry triggerNon-2xx response or connection timeout
OrderingEvents 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.

tip

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-* — triggers verification.completed within ~5 seconds
  • test-fail-* — triggers verification.failed (reason: platform_error)
  • test-expire-* — triggers verification.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

HeaderDescription
X-Creatorlayer-SignatureHMAC-SHA256 signature of the raw body: sha256=<lowercase-hex>
X-Creatorlayer-EventEvent type, e.g. verification.completed, tape.updated
X-Creatorlayer-DeliveryUnique 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.

AttemptDelay after previous failure
1st (initial)Immediate
2nd1 minute
3rd5 minutes
4th30 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-Delivery for 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.