> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bedrock.cv/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive job results via HTTP callbacks instead of polling.

Subscribe to webhooks instead of polling. Configure a webhook endpoint and subscribe to the job types you care about. You'll receive a POST when the job completes or fails.

## Event Payload

The webhook payload wraps the job result:

```json theme={null}
{
  "id": "evt_01JABCD999",
  "type": "parse.completed",
  "timestamp": "2024-01-15T10:01:30Z",
  "data": {
    "job_id": "job_01JABCD123",
    "type": "parse",
    "status": "completed",
    "target": {
      "type": "sheet",
      "id": "sht_01JABCD100"
    },
    "results": {
      "legend": [
        {
          "block_id": "blk_01JABCD200",
          "symbols_detected": 24,
          "symbols_with_graphic": 18,
          "symbols": [
            {
              "feature_id": "ftr_01JABCD300",
              "label": "duplex_receptacle",
              "description": "DUPLEX RECEPTACLE",
              "has_graphic": true
            }
          ]
        }
      ],
      "symbols": [
        {
          "label": "duplex_receptacle",
          "description": "DUPLEX RECEPTACLE",
          "legend_feature_id": "ftr_01JABCD300",
          "instance_count": 4,
          "instances": [
            {
              "feature_id": "ftr_01JABCD400",
              "block_id": "blk_01JABCD100",
              "bounds": { "x_min": 333, "y_min": 207, "x_max": 345, "y_max": 222 },
              "confidence": 0.694
            }
          ]
        }
      ],
      "summary": {
        "total_symbols_searched": 5,
        "total_instances_found": 21
      }
    },
    "completed_at": "2024-01-15T10:01:30Z"
  }
}
```

When a job fails:

```json theme={null}
{
  "id": "evt_01JABCD998",
  "type": "parse.failed",
  "timestamp": "2024-01-15T10:01:30Z",
  "data": {
    "job_id": "job_01JABCD123",
    "type": "parse",
    "status": "failed",
    "target": {
      "type": "sheet",
      "id": "sht_01JABCD100"
    },
    "error": {
      "code": "PROCESSING_ERROR",
      "message": "Unable to process sheet - source image missing"
    },
    "failed_at": "2024-01-15T10:01:30Z"
  }
}
```

## Event Types

Event types follow the pattern `{job_type}.{status}`. This allows subscribing to all status changes for a job type with a single subscription prefix.

| Event             | Description                      |
| ----------------- | -------------------------------- |
| `parse.queued`    | Parse job has been queued        |
| `parse.started`   | Parse job has started processing |
| `parse.completed` | Parse job completed successfully |
| `parse.failed`    | Parse job failed                 |

## Incremental Parse Events

Parse fires `parse.block.completed` events as each block finishes symbol detection, so you can process results incrementally rather than waiting for the entire job:

```json theme={null}
{
  "id": "evt_01JABCD997",
  "type": "parse.block.completed",
  "timestamp": "2024-01-15T10:01:15Z",
  "data": {
    "job_id": "job_01JABCD124",
    "parse_job_id": "job_01JABCD123",
    "block_id": "blk_01JABCD100",
    "type": "parse.block",
    "status": "completed",
    "results": {
      "symbols": [
        {
          "label": "duplex_receptacle",
          "description": "DUPLEX RECEPTACLE",
          "instance_count": 4,
          "instances": [
            {
              "feature_id": "ftr_01JABCD400",
              "bounds": { "x_min": 333, "y_min": 207, "x_max": 345, "y_max": 222 },
              "confidence": 0.694
            }
          ]
        }
      ],
      "summary": {
        "total_symbols_searched": 5,
        "total_instances_found": 21
      }
    },
    "completed_at": "2024-01-15T10:01:15Z"
  }
}
```

The incremental event includes per-block results without the `legend` field (that appears only in the final `parse.completed` event).

## Child Job Events

Long-running parse jobs create internal child jobs (legend parsing, symbol matching). You can subscribe to `parse.child.{status}` events for granular progress tracking:

| Event                   | Description         |
| ----------------------- | ------------------- |
| `parse.child.queued`    | Child job queued    |
| `parse.child.started`   | Child job started   |
| `parse.child.completed` | Child job completed |
| `parse.child.failed`    | Child job failed    |

Child events include `parent_job_id` and `child_job_type` fields for correlation.

## Subscriptions

When configuring a webhook, you subscribe to specific event prefixes. Only events matching your subscribed prefixes are delivered.

## Verifying Signatures

Every webhook request includes an HMAC-SHA256 signature for verification:

| Header                | Description                   |
| --------------------- | ----------------------------- |
| `X-Webhook-Id`        | Unique delivery ID            |
| `X-Webhook-Timestamp` | Unix timestamp of the request |
| `X-Webhook-Signature` | `v1=<hmac-sha256>` signature  |

The signature is computed over `{timestamp}.{payload}` using your webhook secret:

```javascript theme={null}
const crypto = require("crypto");

function verify(payload, headers, secret) {
  const timestamp = headers["x-webhook-timestamp"];
  const expected = headers["x-webhook-signature"].replace("v1=", "");
  const signature = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${payload}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
```

## Retry Behavior

Failed deliveries are retried with exponential backoff up to 5 attempts. After 5 consecutive failures, the webhook endpoint is disabled (circuit breaker). Re-enable it from the Developer settings.
