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

# Webhooks

> Receive a callback when a background request finishes instead of polling for it

When a request can take a while to finish, you don't have to poll `GET /v1/responses/{response_id}` in a loop. Attach a callback URL to the request and Valar delivers the finished result to your server as an HTTP POST. The feature works on `POST /v1/responses` and `POST /v1/chat/completions`, and is configured entirely through two `metadata` keys.

## Parameters

Both keys live inside the `metadata` object on the create request.

<ParamField path="metadata.completion_webhook" type="string">
  The destination URL for the callback. Must be an `http` or `https` URL. If you omit it or pass a value that can't be used, no callback is sent and the request itself is unaffected.
</ParamField>

<ParamField path="metadata.webhook_token" type="string">
  An optional shared secret. Valar sends its value as a Bearer token in the callback's `Authorization` header so your endpoint can confirm the request is genuine.
</ParamField>

## Set up an endpoint

<Steps>
  <Step title="Expose a route that accepts POST">
    Your endpoint receives a JSON body and should respond as soon as it has accepted the payload. The body matches exactly what `GET /v1/responses/{response_id}` returns for the same request, whether the response completed or failed.

    ```python theme={"system"}
    from fastapi import FastAPI, Request

    app = FastAPI()

    @app.post("/hooks/inference-done")
    async def inference_done(request: Request):
        payload = await request.json()
        # payload is the same object GET /v1/responses/{id} returns
        enqueue_for_processing(payload["id"], payload)
        return {"ok": True}
    ```
  </Step>

  <Step title="Verify the Authorization header">
    If you set `webhook_token`, reject any incoming call whose header doesn't match. Compare against `Authorization: Bearer <your token>` and return a 4xx for anything else.

    ```python theme={"system"}
    from fastapi import Header, HTTPException

    WEBHOOK_TOKEN = "whk_3f0a9c2e7b14"

    def assert_authorized(authorization: str = Header(default="")):
        if authorization != f"Bearer {WEBHOOK_TOKEN}":
            raise HTTPException(status_code=401)
    ```
  </Step>

  <Step title="Submit a request that points at your endpoint">
    Set `background=True` and add the callback URL (and token, if you use one) to `metadata`.

    <CodeGroup>
      ```python File 1 theme={"system"}
      from openai import OpenAI

      client = OpenAI(
          base_url="https://api.valarhq.ai/v1", # or read OPENAI_BASE_URL from the environment
          api_key="YOUR_VALAR_API_KEY",  # or read OPENAI_API_KEY from the environment
      )

      response = client.responses.create(
          model="zai-org/GLM-5.2-FP8",
          input="Summarize this document.",
          background=True,
          metadata={
              "completion_webhook": "https://app.example.com/hooks/inference-done",
              "webhook_token": "whk_3f0a9c2e7b14",
          },
      )
      ```
    </CodeGroup>
  </Step>
</Steps>

## What Valar sends

The callback is a single POST request.

<ResponseField name="method" type="POST">
  Always a POST to the URL in `completion_webhook`.
</ResponseField>

<ResponseField name="Content-Type" type="header">
  `application/json`.
</ResponseField>

<ResponseField name="Authorization" type="header">
  Present only when `webhook_token` was set. Carries `Bearer <webhook_token>`.
</ResponseField>

<ResponseField name="body" type="object">
  The full response object, byte-for-byte identical to a `GET /v1/responses/{response_id}` call - including the `status` field and, for failed responses, the `error` envelope. The response `id` lives here and is your key for deduplication.
</ResponseField>

## When a callback fires

Valar POSTs to your endpoint once a response reaches a terminal status:

* **Completed.** The request finished normally. The body is the completed response object.
* **Failed.** The body is the failed response object, including the `error.code` and `error.message` your `GET /v1/responses/{response_id}` would see. This includes responses Valar fails on your behalf when they get stranded short of terminal - for example, a background request that stays `in_progress` past its deadline arrives with `status: "failed"` and `error.code: "timeout"`. Use the `status` field to branch on success vs. failure in your handler.

## Delivery semantics

<Warning>
  The same callback can be delivered more than once. Treat the response `id` in the body as an idempotency key and ignore any `id` you have already processed.
</Warning>

* **Retries.** A delivery that returns a non-2xx status or hits a network error is retried up to **3 times**. Return a **2xx** as soon as you accept the payload to stop the retries.
* **Timeout.** Each attempt has a **30 second** ceiling. A timeout counts as a failed attempt and triggers the next retry.
* **Best-effort.** Callbacks are best-effort. If every attempt fails the failure is logged but never touches the response record or the API, and the result stays available through `GET /v1/responses/{response_id}` regardless of whether delivery ever succeeded.

## Test it locally with ngrok

You can point a real request at a listener on your own machine.

<Steps>
  <Step title="Run a listener that prints the body and returns 200">
    ```bash theme={"system"}
    python -c "
    from http.server import HTTPServer, BaseHTTPRequestHandler; import json
    class H(BaseHTTPRequestHandler):
     def do_POST(self):
      print(json.dumps(json.loads(self.rfile.read(int(self.headers['Content-Length']))), indent=2))
      self.send_response(200); self.end_headers()
    HTTPServer(('127.0.0.1', 8765), H).serve_forever()
    "
    ```
  </Step>

  <Step title="Tunnel to it">
    ```bash theme={"system"}
    ngrok http 8765
    ```

    Copy the `https://xxxx.ngrok-free.app` forwarding URL from the output.
  </Step>

  <Step title="Submit a request against the tunnel">
    ```bash theme={"system"}
    curl -X POST https://api.valarhq.ai/v1/responses \
      -H "Authorization: Bearer YOUR_VALAR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "zai-org/GLM-5.2-FP8",
        "input": "What is 2+2? Reply with just the number.",
        "background": true,
        "metadata": {
          "completion_webhook": "https://xxxx.ngrok-free.app"
        }
      }'
    ```

    When the response finishes, Valar POSTs the full payload to your listener.
  </Step>

  <Step title="Submit a request against the tunnel">
    ```bash theme={"system"}
    curl -X POST https://api.valarhq.ai/v1/responses \
      -H "Authorization: Bearer YOUR_VALAR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "zai-org/GLM-5.2-FP8",
        "input": "What is 2+2? Reply with just the number.",
        "background": true,
        "metadata": {
          "completion_webhook": "https://xxxx.ngrok-free.app"
        }
      }'
    ```

    When the response finishes, Valar POSTs the full payload to your listener.
  </Step>
</Steps>
