Use webhooks after the polling flow works. Webhooks notify your system when a Content Integration job finishes or fails.
Events
| Event | Trigger |
|---|---|
content.completed | A job reached completed. |
content.failed | A job reached failed. |
Create webhook
Set the API base URL and create tutorflow-admin.cookies as described in Content API Key Management. Then discover the organization available to your admin session:
export TUTORFLOW_API_BASE_URL="https://api.tutorflow.io"
export TUTORFLOW_CONTENT_ORG_ID="$(curl -s "$TUTORFLOW_API_BASE_URL/v1/content/organizations" \
-b tutorflow-admin.cookies | jq -r '.[0].id')"WEBHOOK_RESPONSE="$(curl -sS -X POST "$TUTORFLOW_API_BASE_URL/v1/content/organizations/$TUTORFLOW_CONTENT_ORG_ID/webhooks" \
-H "Content-Type: application/json" \
-b tutorflow-admin.cookies \
-d '{
"url": "https://example.com/tutorflow/content-webhooks",
"events": ["content.completed", "content.failed"]
}')"
printf '%s\n' "$WEBHOOK_RESPONSE" | jqResponse:
{
"id": "2f083d42-5c0f-4de6-9c3f-35c4823a589e",
"organizationId": "00000000-0000-4000-8000-000000000001",
"url": "https://example.com/tutorflow/content-webhooks",
"events": ["content.completed", "content.failed"],
"status": "ACTIVE",
"secret": "generated-signing-secret"
}The secret is shown once. Store it in your webhook receiver.
Set local variables from the response:
export TUTORFLOW_CONTENT_WEBHOOK_ID="$(printf '%s' "$WEBHOOK_RESPONSE" | jq -r '.id')"
export TUTORFLOW_CONTENT_WEBHOOK_SECRET="$(printf '%s' "$WEBHOOK_RESPONSE" | jq -r '.secret')"If TUTORFLOW_CONTENT_WEBHOOK_ID is empty or null, inspect WEBHOOK_RESPONSE before continuing.
Request headers
X-Content-Integration-Event: content.completed
X-Content-Integration-Signature: hmac_sha256_signature
Content-Type: application/jsonReceiver behavior
- Verify the signature.
- Return a 2xx response quickly.
- Use the job id in the payload to fetch the result endpoint.
- Treat repeated deliveries as duplicates and handle them idempotently.
Node.js signature verification
import crypto from 'node:crypto'
function verifyContentSignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
)
}List webhooks
curl "$TUTORFLOW_API_BASE_URL/v1/content/organizations/$TUTORFLOW_CONTENT_ORG_ID/webhooks" \
-b tutorflow-admin.cookiesList responses do not include the signing secret.
Delete webhook
curl -X DELETE "$TUTORFLOW_API_BASE_URL/v1/content/organizations/$TUTORFLOW_CONTENT_ORG_ID/webhooks/$TUTORFLOW_CONTENT_WEBHOOK_ID" \
-b tutorflow-admin.cookiesRetry handling
TutorFlow retries failed deliveries. Your receiver should:
- Return 2xx only after accepting the event.
- Avoid side effects before signature verification.
- Store processed event ids or job ids to prevent duplicate processing.
- Fetch the result endpoint if a webhook payload is not enough for your workflow.