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

# Webhook consumers

> Register a webhook consumer to receive ShoulderTap's proposal and request events as HTTP POSTs, and write accepted answers into your system of record.

A webhook consumer receives ShoulderTap's events as HTTP POSTs. This is the standard way to integrate a consumer that runs as its own service: you register a URL, and ShoulderTap POSTs to it whenever something happens to one of your requests.

## Register

Register a consumer with `webhook` delivery. Declare the kinds it handles and the URL to POST to:

```bash theme={null}
curl -X POST http://localhost:8776/api/v1/consumers \
  -H "Authorization: Bearer $SHOULDERTAP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "my-consumer",
    "handles_kinds": ["glossary.definition", "freeform.answer"],
    "delivery": {"type": "webhook", "url": "https://my-service.example.com/shouldertap-webhook"},
    "dedup_window": "PT24H"
  }'
```

See the [Consumers endpoint](/api/consumers) for every registration field, including `auto_accept` and custom `kind_schemas`.

## Event payloads

Each POST body has an `event` field naming what happened, plus the relevant data:

<CodeGroup>
  ```json proposal_accepted theme={null}
  {
    "event": "proposal_accepted",
    "proposal": { "...ContextProposal..." }
  }
  ```

  ```json proposal theme={null}
  {
    "event": "proposal",
    "proposal": { "...ContextProposal..." }
  }
  ```

  ```json proposal_rejected theme={null}
  {
    "event": "proposal_rejected",
    "proposal": { "...ContextProposal..." },
    "reason": "not accurate"
  }
  ```

  ```json request_failed theme={null}
  {
    "event": "request_failed",
    "request_id": "req_01J...",
    "reason": { "code": "timeout" }
  }
  ```
</CodeGroup>

The one that matters is **`proposal_accepted`** — that's when a human has approved the answer and you should write it to your system of record. The `proposal` object carries the expert's verbatim `answer`, the LLM-`structured` form, and `provenance`. See the [ContextProposal contract](/api/contracts).

## Handle the webhook

A minimal handler validates the payload and acts only on acceptance:

```python theme={null}
from fastapi import FastAPI, Request
from shouldertap.engine.contracts import ContextProposal

app = FastAPI()

@app.post("/shouldertap-webhook")
async def webhook(request: Request):
    payload = await request.json()
    if payload.get("event") != "proposal_accepted":
        return {"ignored": True}
    proposal = ContextProposal.model_validate(payload["proposal"])
    # write proposal.structured (or proposal.answer) to your system of record
    return {"ok": True}
```

<Note>
  `ContextProposal` does not carry the original request's `context`. If your write-back needs to know which entity a request mapped to (for example, which glossary term to update), track that mapping on the consumer side, keyed by `request_id`. The `examples/openmetadata_sync/` consumer in the repository shows this pattern with a small `/remember` endpoint.
</Note>

## Write-back adapters

Rather than writing the delivery-to-system-of-record logic yourself, you can use a built-in adapter that turns an accepted proposal into a write against OpenMetadata or any webhook target. See [Write-back adapters](/consumers/write-back).
