Skip to content
JARAI Developers

Webhook Reference

Complete reference for the JARAI webhook system. Covers the event envelope, signature verification, all 9 event types with payload schemas, delivery behaviour, and schema stability guarantees.

Event Envelope

Every webhook delivery sends a JSON POST request with the following envelope structure:

{
  "eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "eventType": "production.published",
  "productionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-04-03T14:30:00Z",
  "sequenceNumber": 12,
  "correlationId": "corr-abc123-def456",
  "truncated": false,
  "data": { }
}
FieldTypeDescription
eventIdUUIDUnique identifier for this event delivery.
eventTypestringEvent type identifier (e.g. production.published).
productionIdUUIDThe production this event relates to. Nil UUID for webhook.test.
accountIdUUIDThe account that owns the production. Nil UUID for webhook.test.
timestampstringISO 8601 UTC timestamp of when the event occurred.
sequenceNumberintegerPer-production event counter. Use with productionId for idempotency.
correlationIdstringTrace identifier for debugging with JARAI support.
truncatedbooleantrue only on deliverable.ready when the deliverables list exceeds 100 items.
dataobjectEvent-specific payload. Structure varies by event type.

Signature Verification

Every webhook delivery includes an X-JARAI-Signature header for authenticating that the request originated from JARAI.

Header Format

X-JARAI-Signature: sha256=a1b2c3d4e5f6789012345678abcdef...

Verification Steps

  1. Read the raw HTTP request body as bytes. Do not parse and re-serialize the JSON — re-serialization may alter whitespace or field ordering.
  2. Compute HMAC-SHA256 using your signingSecret (base64url-decoded) as the key and the raw body bytes as the message.
  3. Hex-encode the resulting digest.
  4. Compare with the value after sha256= in the header using a constant-time comparison to prevent timing attacks.
using System.Security.Cryptography;
using System.Text;

public static bool VerifySignature(
    byte[] requestBody,
    string signatureHeader,
    string signingSecretBase64Url)
{
    // Decode the base64url signing secret
    string base64 = signingSecretBase64Url
        .Replace('-', '+')
        .Replace('_', '/');
    switch (base64.Length % 4)
    {
        case 2: base64 += "=="; break;
        case 3: base64 += "="; break;
    }
    byte[] secret = Convert.FromBase64String(base64);

    // Compute HMAC-SHA256
    using var hmac = new HMACSHA256(secret);
    byte[] hash = hmac.ComputeHash(requestBody);
    string expected = "sha256="
        + Convert.ToHexString(hash).ToLowerInvariant();

    // Constant-time comparison
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(signatureHeader));
}

If verification fails, return a 4xx status code and do not process the event.

Event Types

production.queued

Trigger: Production submitted to the pipeline.

{
  "eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "eventType": "production.queued",
  "productionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-04-03T10:30:00Z",
  "sequenceNumber": 1,
  "correlationId": "corr-abc123",
  "truncated": false,
  "data": {
    "status": "Queued",
    "expiresAt": "2026-04-03T10:40:00Z"
  }
}
FieldTypeDescription
data.statusstringAlways "Queued".
data.expiresAtstring | nullISO 8601 UTC expiry timestamp for API-triggered productions. null for non-API productions.

production.step.complete

Trigger: A pipeline step finished processing.

{
  "eventId": "b7e3a9d1-4c2f-4e8b-9f1a-3d5c7e9b0a2f",
  "eventType": "production.step.complete",
  "productionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-04-03T10:35:22Z",
  "sequenceNumber": 2,
  "correlationId": "corr-abc123",
  "truncated": false,
  "data": {
    "status": "Producing",
    "stepsComplete": 5,
    "currentStep": "Image Acquisition"
  }
}
FieldTypeDescription
data.statusstringAlways "Producing".
data.stepsCompleteintegerNumber of pipeline steps completed so far.
data.currentStepstringHuman-readable label of the step that just completed.

production.awaiting_approval

Trigger: All pipeline gates passed. Deliverables available for review, awaiting operator release.

{
  "eventId": "c8f4b0e2-5d3a-4f9c-a02b-4e6d8f0c1b3a",
  "eventType": "production.awaiting_approval",
  "productionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-04-03T10:52:10Z",
  "sequenceNumber": 8,
  "correlationId": "corr-abc123",
  "truncated": false,
  "data": {
    "status": "AwaitingApproval",
    "stepsComplete": 15
  }
}
FieldTypeDescription
data.statusstringAlways "AwaitingApproval".
data.stepsCompleteintegerTotal pipeline steps completed.

production.distributing

Trigger: First platform publication started.

{
  "eventId": "d9a5c1f3-6e4b-4a0d-b13c-5f7e9a1d2c4b",
  "eventType": "production.distributing",
  "productionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-04-03T11:05:30Z",
  "sequenceNumber": 9,
  "correlationId": "corr-abc123",
  "truncated": false,
  "data": {
    "status": "Distributing",
    "platforms": [
      { "platform": "YouTube", "status": "Publishing" },
      { "platform": "TikTok", "status": "Pending" }
    ]
  }
}
FieldTypeDescription
data.statusstringAlways "Distributing".
data.platformsarrayPer-platform publication status.
data.platforms[].platformstringPlatform name.
data.platforms[].statusstringPlatform-level status (e.g. "Publishing", "Pending").

production.published

Trigger: All platforms published successfully. This is a terminal state.

{
  "eventId": "e0b6d2a4-7f5c-4b1e-c24d-6a8f0b2e3d5c",
  "eventType": "production.published",
  "productionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-04-03T11:12:45Z",
  "sequenceNumber": 10,
  "correlationId": "corr-abc123",
  "truncated": false,
  "data": {
    "status": "Published",
    "platforms": [
      { "platform": "YouTube", "status": "Published", "platformPostId": "dQw4w9WgXcQ" },
      { "platform": "TikTok", "status": "Published", "platformPostId": "7340912345678901234" }
    ]
  }
}
FieldTypeDescription
data.statusstringAlways "Published".
data.platformsarrayFinal per-platform publication status.
data.platforms[].platformstringPlatform name.
data.platforms[].statusstringAlways "Published" at this stage.
data.platforms[].platformPostIdstring | nullPlatform-assigned post/video identifier. null if the platform does not provide one.

production.failed

Trigger: Production failed terminally. This is a terminal state.

{
  "eventId": "f1c7e3b5-8a6d-4c2f-d35e-7b9a1c3f4e6d",
  "eventType": "production.failed",
  "productionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-04-03T10:42:18Z",
  "sequenceNumber": 4,
  "correlationId": "corr-abc123",
  "truncated": false,
  "data": {
    "status": "Failed",
    "errorCode": "PROVIDER_UNAVAILABLE",
    "errorMessage": "AI provider returned HTTP 503 after exhausting retry attempts."
  }
}
FieldTypeDescription
data.statusstringAlways "Failed".
data.errorCodestringMachine-readable error code. See Production Error Codes.
data.errorMessagestringHuman-readable error description.

production.cancelled

Trigger: Production cancelled by partner or operator. This is a terminal state.

{
  "eventId": "a2d8f4c6-9b7e-4d3a-e46f-8c0b2d4a5f7e",
  "eventType": "production.cancelled",
  "productionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-04-03T10:38:05Z",
  "sequenceNumber": 3,
  "correlationId": "corr-abc123",
  "truncated": false,
  "data": {
    "status": "Cancelled"
  }
}
FieldTypeDescription
data.statusstringAlways "Cancelled".

deliverable.ready

Trigger: Deliverable files are available for download.

{
  "eventId": "b3e9a5d7-0c8f-4e4b-f57a-9d1c3e5b6a8f",
  "eventType": "deliverable.ready",
  "productionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-04-03T10:52:10Z",
  "sequenceNumber": 7,
  "correlationId": "corr-abc123",
  "truncated": false,
  "data": {
    "status": "AwaitingApproval",
    "deliverables": [
      {
        "deliverableId": "d1e2f3a4-b5c6-7890-d1e2-f3a4b5c67890",
        "type": "Video",
        "platform": "YouTube",
        "format": "mp4"
      },
      {
        "deliverableId": "e2f3a4b5-c6d7-8901-e2f3-a4b5c6d78901",
        "type": "ClosedCaptions",
        "platform": "YouTube",
        "format": "srt"
      },
      {
        "deliverableId": "f3a4b5c6-d7e8-9012-f3a4-b5c6d7e89012",
        "type": "Transcript",
        "platform": null,
        "format": "txt"
      },
      {
        "deliverableId": "a4b5c6d7-e8f9-0123-a4b5-c6d7e8f90123",
        "type": "ComplianceCertificate",
        "platform": null,
        "format": "pdf"
      }
    ]
  }
}
FieldTypeDescription
data.statusstringCurrent production status (typically "AwaitingApproval").
data.deliverablesarrayList of available deliverable files.
data.deliverables[].deliverableIdUUIDUnique identifier for this deliverable.
data.deliverables[].typestringOne of: Video, ClosedCaptions, AudioDescription, Transcript, ComplianceCertificate.
data.deliverables[].platformstring | nullPlatform name for platform-specific deliverables (e.g. YouTube, TikTok). null for production-level deliverables.
data.deliverables[].formatstringFile format: mp4, srt, vtt, txt, docx, or pdf.

Note: The deliverable.ready event does not include SAS download URLs. Call GET /v1/productions/{productionId}/deliverables to obtain time-limited download URLs.

If the production has more than 100 deliverables, the truncated field in the envelope is true and only the first 100 items are included. Use the deliverables API endpoint to retrieve the full list.

webhook.test

Trigger: Ping test sent via POST /v1/webhooks/{subscriptionId}/ping.

{
  "eventId": "c4f0b6e8-1d9a-4f5c-a68b-0e2d4f6a7c9e",
  "eventType": "webhook.test",
  "productionId": "00000000-0000-0000-0000-000000000000",
  "accountId": "00000000-0000-0000-0000-000000000000",
  "timestamp": "2026-04-03T14:00:00Z",
  "sequenceNumber": 0,
  "correlationId": "corr-ping-test-001",
  "truncated": false,
  "data": {
    "type": "webhook.test",
    "message": "This is a test event from the JARAI Partner API."
  }
}
FieldTypeDescription
data.typestringAlways "webhook.test".
data.messagestringHuman-readable test message.

productionId and accountId are set to nil UUIDs (00000000-0000-0000-0000-000000000000) for test events.

Delivery Behaviour

Retry Schedule

JARAI guarantees at-least-once delivery via Azure Service Bus. If your endpoint does not return a 2xx response, the event is retried:

AttemptTiming
1st retryImmediate
2nd retry1 minute after first failure
3rd retry5 minutes after second failure

After all three retries fail, the delivery counts as one failure and the subscription's failureCount is incremented.

Failure Counting and Auto-Suspension

Consecutive FailuresAction
3WebhookDeliveryFailed warning alert raised internally.
10Subscription status set to Failed. No further events delivered. WebhookEndpointUnreachable alert raised.

A successful delivery (2xx response) resets the failure counter to zero.

In-Flight Deletion

If you delete a webhook subscription while events are already queued for delivery, those in-flight events will be delivered normally. No new events are dispatched after the deletion commits. Subscriptions are logically deleted (never physically removed) for audit trail purposes.

Schema Stability Contract

JARAI commits to the following guarantees for webhook payloads:

  • Additive-only changes — new fields may be added to event payloads at any time. Existing fields will not be removed or have their types changed without notice.
  • 6-month deprecation notice — if a field or event type is to be removed, JARAI will provide at least 6 months of advance notice via the X-API-Deprecation-Notice response header and developer portal announcements.
  • Ignore unknown fields — your webhook handler should ignore any fields it does not recognise. Do not fail on unexpected fields.

Idempotency Guidance

Because JARAI guarantees at-least-once delivery, your endpoint may receive the same event more than once. Use the composite key {productionId}:{sequenceNumber} to deduplicate events.

The sequenceNumber is scoped to a single production (not globally unique). Store processed composite keys and skip any event where the key has already been seen.

# Example: idempotency check with a set (use Redis or DB in production)
processed_events = set()

def handle_webhook(event: dict) -> bool:
    key = f"{event['productionId']}:{event['sequenceNumber']}"
    if key in processed_events:
        return False  # Already processed — skip

    processed_events.add(key)
    # Process the event...
    return True