Brand key required
Webhooks
Events are POSTed to your endpoint as they happen, signed with HMAC-SHA256. Poll nothing.
Subscribe
POST/v1/webhooks
| Field | Type | Description |
|---|---|---|
| url | string | Required. Must be an https:// URL. |
| events | string[] | Required, at least one event from the table below. Unknown event names are rejected with 422. |
request
curl -X POST "https://api.ugcroster.com/v1/webhooks" \
-H "Authorization: Bearer rsk_brand_key" \
-H "Content-Type: application/json" \
-d '{ "url": "https://yourapp.com/hooks/roster", "events": ["deliverable.submitted", "commission.created"] }'response 200
{
"data": {
"id": "wh_new001",
"url": "https://yourapp.com/hooks/roster",
"events": ["deliverable.submitted", "commission.created"],
"secret": "whsec_1f6c…9e03",
"active": true,
"created_at": "2026-06-13T16:50:00Z"
}
}The full signing secret is returned only on creation. Store it immediately. The list endpoint shows a truncated version. Each brand can hold up to 10 subscriptions.
Events
| Event | Type | Description |
|---|---|---|
| application.received | creator | A creator applied to one of your campaigns. |
| application.approved | brand | An application was approved. |
| commission.created | system | A new commission event was attributed to a creator. |
| commission.approved | brand | A commission was approved for payout. |
| payout.created | brand | A payout was recorded. |
| content.refreshed | system | Tracked-post metrics were refreshed. |
| message.received | creator | A creator sent you a message. |
| deliverable.submitted | creator | A creator uploaded content against a deliverable. |
| deliverable.approved | brand | A submission was approved in review. |
| deliverable.revision_requested | brand | A submission was sent back with notes. |
| deliverable.declined | creator | A creator declined an assigned deliverable. |
| deliverable.cancelled | brand | A deliverable was cancelled before completion. |
Manage subscriptions
GET/v1/webhooks
response 200
{
"data": [
{
"id": "wh_001",
"url": "https://yourapp.com/hooks/roster",
"events": ["deliverable.submitted", "commission.created"],
"active": true,
"secret": "whsec_1f…9e03",
"created_at": "2026-05-01T10:00:00Z",
"last_triggered_at": "2026-06-13T12:00:00Z",
"last_status_code": 200
}
],
"pagination": { "page": 1, "limit": 20, "total": 2, "has_more": false }
}last_triggered_at and last_status_code tell you whether your endpoint is receiving and acknowledging deliveries.
DELETE/v1/webhooks/{id}
request
curl -X DELETE "https://api.ugcroster.com/v1/webhooks/wh_001" \
-H "Authorization: Bearer rsk_brand_key"Delivery format
POST to your endpoint
Headers:
X-Roster-Signature: sha256=<hex HMAC-SHA256 of the raw body>
Body:
{ "event": "deliverable.submitted", "createdAt": "2026-09-05T21:04:11.000Z", "data": { … } }Verify the signature
Every delivery is signed with your webhook secret. Compute the HMAC over the raw request body, before any JSON parsing: re-serialising the parsed object can change key order and break the comparison.
node.js
import crypto from 'crypto';
function verify(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}python
import hmac
import hashlib
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature_header, expected)
# In your handler:
is_valid = verify(request.body, request.headers["X-Roster-Signature"], "whsec_your_secret")Always use a constant-time comparison (timingSafeEqual / compare_digest) and reject anything that fails verification before touching the payload.
Timeouts and retries
| Rule | Type | Description |
|---|---|---|
| timeout | 5 seconds | Each delivery attempt must get a response within 5 seconds. |
| retry | once, after 2s | On a network error or a 5xx response, the delivery is retried once after 2 seconds. 4xx responses are not retried. |
| observability | per subscription | The most recent outcome is exposed as last_status_code on GET /v1/webhooks. |
The budget is tight by design: respond 2xx immediately and process the event asynchronously. If your endpoint misses both attempts the event is not redelivered. Treat webhooks as a trigger to fetch current state from the API, not as the source of truth.