Events and jobs¶
Successful transcription commits write their completion event and webhook deliveries in the same transaction as the canonical page. They do not publish the resulting draft. An explicit, revision-checked publication transaction updates the public snapshot and a coalescing, revision-fenced Triplet mirror outbox row, and writes its publication event and webhook deliveries. Dispatchers retry network delivery; consumers must be idempotent.
Workspace webhook subscriptions¶
Webhook targets are workspace-owned API resources, not a process-global URL
list. A workspace administrator uses WebhookService.CreateWebhook,
ListWebhooks, and DeleteWebhook. Creation accepts a positive
workspace_id, one absolute public HTTPS target without credentials, query, or
fragment, and a caller-generated signing secret containing 32–1024 bytes
without surrounding whitespace. Store the secret in the receiver's secret
manager before creation: it is write-only and never appears in create or list
responses. A workspace may register at most 100 unique targets.
Scribe does not follow delivery redirects; the registered target must accept
the POST directly.
The 0002_islandora_editor_review upgrade does not convert queued deliveries
from the removed process-global URL configuration. Those attempts were
unsigned and have no receiver-known secret, so converting them would create a
subscription that no receiver could authenticate. The migration retains their
event-outbox records, removes the incompatible delivery attempts, and starts
delivery only for explicit subscriptions created after the upgrade.
Each event transaction expands deliveries only for subscriptions belonging to the event's workspace. Expansion and subscription creation or deletion serialize on that workspace, so a concurrent delete cannot leave a delivery without its subscription. Deletion removes every delivery for the subscription before removing the subscription itself, including terminal delivery history; the parent event remains until normal bounded event retention removes it. It never exposes or affects another workspace's target.
Verify a delivery¶
Scribe sends the structured CloudEvent JSON as the exact POST body with:
Content-Type: application/cloudevents+jsonX-Scribe-Timestamp: <Unix seconds>X-Scribe-Signature: v1=<lowercase hexadecimal HMAC-SHA256>
The signed bytes are:
Verify before JSON decoding or any repository side effect:
- Read the bounded raw body without reserializing it. Require a decimal Unix timestamp and reject a timestamp outside the receiver's small replay window, normally five minutes.
- Require exactly the supported
v1=signature form. Compute HMAC-SHA256 with the subscription secret over the timestamp bytes, one ASCII period, and the unmodified body; encode the digest as lowercase hexadecimal. - Compare the complete expected and supplied signature in constant time. Missing, malformed, stale, or mismatched values are unauthenticated and must not reach event handling.
- After verification, decode the CloudEvent and atomically record its
idwith the intended repository action. Return 2xx only after that durable idempotency boundary commits.
Every retry has a freshly generated timestamp and signature. Network failures
and non-2xx responses are retried, so the same CloudEvent id can arrive more
than once. Do not deduplicate by timestamp or assume delivery order; use the
event ID for attempt deduplication and the resource's monotonic revision for
state ordering.
Event correlation data¶
Item-scoped completion, terminal failure, and publication events carry the
same correlation spine in data:
- numeric
workspaceIdanditemImageId; - string
itemIdandcanvasUri; - numeric canonical
revision; and - caller values
externalReferenceId,idempotencyKey, andmetadatawhen the ingest supplied them.
dev.scribe.transcription.completed also includes jobId, attemptNumber,
completedSegments, failedSegments, and totalSegments.
Completion may be partial after any individual line is unreadable or has a
retryable or unclassified provider, image-pipeline, or validation failure,
provided at least one other line succeeds. completedSegments + failedSegments
then equals totalSegments, and each failed line remains unchanged in the new
canonical revision. A wholly unsuccessful retryable or unclassified attempt
retries the fenced job; cancellation and permanent provider failures stop
immediately without committing partial output.
dev.scribe.transcription.failed includes the job and claimed-attempt IDs plus
a bounded, redacted error; cancellation includes the current attempt number.
The transient dev.scribe.transcription.task.started and .task.completed
previews carry that same attempt number so clients can fence retry visuals.
dev.scribe.annotations.published adds annotationCount,
annotationPageId, publishedRevision, publicUrl, and publishedAt.
Treat the IDs as one workspace-scoped tuple and validate them against the
integration's persisted ingest record. Never perform a lookup by Canvas URI
alone.
Long-running transcription APIs return a job identity. Job status distinguishes pending, running, completed, failed, and canceled work. Retry and supersession details are represented by attempt counters and the redacted failure reason. Per-segment payloads are transient progress previews, not durable canonical saves.
ListTranscriptionJobs returns scalar summaries through signed, opaque keyset
cursors. Pages default to 50 jobs and are capped at 100; a continuation token
is bound to the selected workspace and optional item-image filter. Summary rows
exclude context snapshots, per-segment annotation JSON, and attempt history so
the page count is also a meaningful response-cost bound. Use
GetTranscriptionJob for one job's full progress payload and immutable attempt
audit.
Server-sent event streams are bounded per principal/workspace and send only
authorized job updates. Every delivered event has a numeric outbox id; a
reconnecting EventSource sends it as Last-Event-ID and the server resumes
strictly after that cursor. The named dev.scribe.stream.ready event records
the initial/reconnected high-water mark. Clients subscribe first, then reconcile
their durable job snapshot after each ready event so completion cannot fall
between a snapshot and subscription. They still load the canonical page
revision rather than treating an event payload as a save.
SSE delivery is replica-safe because it is not an in-process broadcast. Every
API replica independently reads the shared, workspace-filtered event outbox. A
reconnect may reach any replica and resumes from the same monotonic outbox ID.
When filters hide every event in a polled batch, a
dev.scribe.stream.checkpoint frame still advances the browser cursor past
that batch. A new connection without Last-Event-ID starts at the current
workspace high-water mark by design, so clients must use the ready-then-snapshot
sequence above rather than expecting historical replay.
The authenticated server-sent event stream and outbound webhooks are separate delivery contracts. SSE uses the stream cursor and the caller's existing Scribe credential; the HMAC headers above apply only to outbound webhook POSTs.