---
title: Handle jobs and events
description: Verify completion events, deduplicate deliveries, and recover state.
doc_version: 0.1.0-preview
last_updated: 2026-09-14
---

# Handle jobs and events

Use signed webhook events for live completion. Persist event IDs to handle duplicate deliveries, and read current resource state after a missed notification.

## Configure your receiver

Create an HTTPS endpoint in your application. Ask the cluster operator to configure its URL, a unique name, and a signing secret. This webhook configuration is separate from API keys. There is no webhook CRUD endpoint.

**Example operator configuration**

```yaml
events:
  webhooks:
    - name: acme-service
      url: https://app.example.com/engine-events
      secret: "<WEBHOOK_SIGNING_SECRET>"
      types:
        - job.updated
        - file.status_changed
        - collection.status_changed
```

The actual signing secret starts with `whsec_`. It belongs to this receiver and must not be an Engine API key. Omitting `types` subscribes to all supported event types; an empty list is invalid. The operator reloads configuration with a restart.

Call `/info` to check the webhook count and supported event types. Invalid receiver configuration makes `/ready` return `503`. The maximum is eight configured receivers. A new receiver starts with new events and does not replay past events.

## Connect a job to an event

*Illustration: Save the job and file IDs from the write response. Match a verified event to those IDs. Use a recovery GET if a notification is missed.*

Save the job ID from each accepted write before updating your UI. Match `job_id` or `file_id` from the event to that operation. Use `namespace` and `collection` to route the status to the correct tenant.

**Example event body**

```json
{
  "id": "evt_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
  "type": "file.status_changed",
  "occurred_at": "2026-10-06T12:01:00Z",
  "cluster": "graphon-engine",
  "namespace": "acme",
  "collection": "incident-logs",
  "job_id": "job_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
  "file_id": "fil_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
  "data": {
    "status": "ready",
    "previous_status": "indexing"
  }
}
```

`job.updated` reports operation status. `file.status_changed` reports file readiness. `collection.status_changed` reports the collection summary. Initial job and file events have `data.previous_status: null`. A new collection in the `empty` state does not emit a creation event. Search and Query do not emit completion webhooks.

## Verify before parsing

| Header | Type | Meaning |
| --- | --- | --- |
| `Content-Type` | string | `application/json`. Verify the original body bytes before parsing. |
| `X-Graphon-Events-Signature` | string | `sha256=` followed by the hex HMAC-SHA256 digest. |
| `X-Graphon-Events-Timestamp` | string containing Unix seconds | The delivery timestamp used in signature verification. |
| `X-Graphon-Events-Delivery` | string | A del_ identifier unique to this attempt at this webhook. |
| `X-Graphon-Events-Webhook` | string | The receiver’s configured name. |

Select the receiver’s secret from trusted server configuration. Build the signed message from the timestamp header, a dot, and the exact raw body bytes. Compute HMAC-SHA256 and compare digests in constant time.

Reject a missing or malformed signature. Reject a timestamp more than 300 seconds before or after the current time. Re-encoding parsed JSON changes the signed bytes, so verify before parsing.

**Python · verify one delivery**

```python
import hashlib
import hmac
import json
import time

def verify_event(raw_body: bytes, timestamp: str, signature: str, secret: str):
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("Webhook timestamp is outside the allowed window.")
    message = timestamp.encode("ascii") + b"." + raw_body
    digest = hmac.new(secret.encode("utf-8"), message, hashlib.sha256).hexdigest()
    expected = "sha256=" + digest
    if not hmac.compare_digest(expected, signature):
        raise ValueError("Webhook signature does not match.")
    return json.loads(raw_body)
```

Call this function with the raw request body and the two signature headers. Reject conversion, decoding, and verification failures. The function verifies a delivery; your receiver must also deduplicate and authorize tenant access.

## Deduplicate and acknowledge

Event `id` is stable across retries and receivers. The delivery header changes for each attempt. Store the event ID with the accepted work. If that ID already exists, acknowledge the duplicate without repeating its effects.

Accept the event into durable application storage or a queue before returning `2xx`. Perform slower follow-up work after acceptance. Respond within 10 seconds. Apply your application’s access checks before showing a tenant’s event to a person.

A receiver can receive events from multiple namespaces. Filtering by namespace happens after signature verification. A valid signature identifies the configured cluster delivery; it does not sign a human into your application.

## Handle retries

| Receiver result | Delivery behavior |
| --- | --- |
| 2xx | Delivery succeeds. Engine stops attempts for this event and receiver. |
| 429 or 5xx | Engine retries this receiver. |
| Network error or timeout | Engine retries this receiver. |
| 3xx | Delivery fails. Engine follows no redirect and does not retry. |
| Other 4xx | Delivery fails without a retry. |

There are at most eight attempts: the original POST and seven retries. Retry delays are 1, 2, 4, 8, 16, 32, and 64 seconds. A failed receiver does not block delivery to another receiver.

Engine delivers events sequentially for each receiver. A retry delays later events for that receiver. After the final failed attempt, Engine stops delivering that event to that URL.

## Recover after a missed event

Use the saved job ID for a one-time status read after a missed event, a receiver outage, or a reconnect. Do not use a GET loop as the live notification path.

**GET https://engine.example.com/jobs/job_01J8Z3K4N5P6Q7R8S9T0V1W2X3**

```http
GET /jobs/job_01J8Z3K4N5P6Q7R8S9T0V1W2X3 HTTP/1.1
Host: engine.example.com
Authorization: Bearer <GRAPHON_ENGINE_API_KEY>
```

**200 OK**

```json
{
  "id": "job_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
  "type": "index_file",
  "status": "succeeded",
  "namespace": "acme",
  "collection": "incident-logs",
  "file_id": "fil_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
  "error": null,
  "created_at": "2026-10-06T12:00:00Z",
  "updated_at": "2026-10-06T12:01:00Z"
}
```

A failed job includes `error.code` and `error.message`. Correct the request or source before another write. A `succeeded` job confirms completion. Read the file or collection if your UI needs its latest fields.

See [Get job](/engine/reference/get-job), [Cancel job](/engine/reference/cancel-job), and the [events webhook contract](/engine/reference/events).
## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
