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

# Webhooks

> Real-time event notifications for submissions, catalog changes, and collections.

## Registering a webhook

```http theme={null}
POST /api/v1/webhooks
Authorization: Bearer edp_live_<key>
Content-Type: application/json

{
  "url": "https://yourplatform.com/webhooks/edpire",
  "events": ["submission.graded", "assessment.published"]
}
```

The response includes a `secret` — **store it securely**. It is shown only once.

## Signature verification

Every webhook delivery includes an `X-Edpire-Signature` header. Always verify it before processing:

<CodeGroup>
  ```typescript Node.js / TypeScript theme={null}
  import { createHmac, timingSafeEqual } from "crypto"

  function verifySignature(
    rawBody: string,
    signature: string | null,
    secret: string
  ): boolean {
    if (!signature) return false
    const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`
    if (signature.length !== expected.length) return false
    return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  }

  // Express example
  app.post("/webhooks/edpire", express.raw({ type: "application/json" }), (req, res) => {
    const sig = req.headers["x-edpire-signature"] as string
    if (!verifySignature(req.body.toString(), sig, process.env.EDPIRE_WEBHOOK_SECRET!)) {
      return res.sendStatus(401)
    }
    res.sendStatus(200) // respond immediately
    const payload = JSON.parse(req.body.toString())
    // process event...
  })
  ```

  ```python Python theme={null}
  import hmac, hashlib

  def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = "sha256=" + hmac.new(
          secret.encode(), raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)
  ```

  ```php PHP theme={null}
  function verifySignature(string $rawBody, string $signature, string $secret): bool {
      $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
      return hash_equals($expected, $signature);
  }
  ```
</CodeGroup>

## Available events

| Event                          | Triggered when                                                        | Key payload fields                                                                                                                                 |
| ------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `submission.graded`            | Learner submits and auto-grading completes                            | `submission_id`, `assessment_id`, `learner_ref`, `score`, `max_score`, `percentage`, `passed`, `awaiting_manual_grading`, `submitted_at`, `source` |
| `submission.grading.completed` | A teacher finishes grading the open-response answers on a submission  | `submission_id`, `assessment_id`, `learner_ref`, `score`, `max_score`, `percentage`, `passed`, `auto_score`, `manual_score`, `graded_at`           |
| `assessment.published`         | Assessment published for the first time                               | `assessment_id`, `share_code`, `title`                                                                                                             |
| `assessment.content_updated`   | Published assessment content is updated (republish or manual trigger) | `assessment_id`, `title`                                                                                                                           |
| `assessment.archived`          | Assessment archived                                                   | `assessment_id`                                                                                                                                    |
| `assessment.updated`           | Assessment title/description/settings changed                         | `assessment_id`, `title`                                                                                                                           |
| `collection.created`           | New collection created                                                | `collection_id`, `name`                                                                                                                            |
| `collection.updated`           | Collection name/description changed                                   | `collection_id`, `name`                                                                                                                            |
| `collection.deleted`           | Collection deleted                                                    | `collection_id`                                                                                                                                    |
| `collection.item_added`        | Assessment added to a collection                                      | `collection_id`, `assessment_id`, `assessment_title`, `share_code`, `position`                                                                     |
| `collection.archived`          | Collection archived                                                   | `collection_id`                                                                                                                                    |
| `collection.item_removed`      | Assessment removed from a collection                                  | `collection_id`, `assessment_id`                                                                                                                   |
| `collection.reordered`         | Collection items reordered                                            | `collection_id`, `item_ids`                                                                                                                        |

## Assessments with open-ended questions

<Warning>
  If an assessment contains open-response questions, `submission.graded` fires with
  **`awaiting_manual_grading: true`**, and its `score`, `percentage` and `passed` cover
  **only the auto-graded questions**. They are provisional.

  Do not store them as a final grade and do not show them to the learner as one. Wait for
  **`submission.grading.completed`**, which carries the real total once a teacher has graded,
  broken down into `auto_score` and `manual_score`.

  For fully auto-graded assessments `awaiting_manual_grading` is `false` and
  `submission.graded` is final, so no second event arrives.
</Warning>

The same signal is available on the REST surface: `POST /assessments/{id}/submit` returns
`awaiting_manual_grading`, and the results and submission endpoints return `is_fully_graded`.

## Retry behavior

If your endpoint returns a non-2xx status, Edpire retries with exponential backoff:

| Attempt   | Delay      |
| --------- | ---------- |
| 1st retry | 1 minute   |
| 2nd retry | 5 minutes  |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours    |

After the initial delivery plus 4 retries, the delivery is abandoned and the webhook endpoint is marked as `failing`. Use `GET /api/v1/webhooks/deliveries` to inspect delivery history and diagnose failures.

## Best practices

<AccordionGroup>
  <Accordion title="Respond with 200 immediately">
    Respond before processing. Move heavy logic to a background queue to avoid timeouts.
  </Accordion>

  <Accordion title="Handle idempotency">
    The same event can be delivered more than once. Always check whether you've already processed a given `submission_id` before writing to your database.
  </Accordion>

  <Accordion title="Log everything">
    Log all incoming events for debugging. The `X-Edpire-Event` header tells you the event type without parsing the body.
  </Accordion>

  <Accordion title="Handle unknown events">
    New event types may be added in future versions. Ignore events you don't recognize rather than erroring.
  </Accordion>
</AccordionGroup>
