Webhooks

Receive a callback when a background response finishes instead of polling for it.

Webhooks

When a request can take a while to finish, you do not have to poll GET /v1/responses/{response_id} in a loop. Attach a callback URL to the request and sference 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.

Beta

Webhook delivery is in beta and enabled per account. If you set the metadata keys and no callback arrives, check with your account team — the request itself is never affected by webhook configuration.

Parameters

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

KeyTypeMeaning
metadata.completion_webhookstringDestination URL for the callback. Must be http or https. If omitted or unusable, no callback is sent and the request is unaffected.
metadata.webhook_tokenstringOptional shared secret. sference sends its value as Authorization: Bearer <token> so your endpoint can verify the call is genuine.

Set up an endpoint

1. 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 what GET /v1/responses/{response_id} returns for the same request, whether the response completed or failed.

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}

2. Verify the Authorization header

If you set webhook_token, reject any call whose header does not match:

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)

3. Submit a request that points at your endpoint

Set background: true and add the callback URL (and token, if you use one) to metadata:

from openai import OpenAI

client = OpenAI(base_url="https://api.sference.com/v1", api_key=SFERENCE_KEY)

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

What sference sends

A single POST request.

PartValue
MethodPOST to the URL in completion_webhook
Content-Typeapplication/json
AuthorizationPresent only when webhook_token was set: Bearer <webhook_token>
BodyThe full response object, identical to GET /v1/responses/{response_id} — including status and, for failures, the error envelope. The response id is in the body and is your deduplication key.

When a callback fires

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

  • Completed — the request finished normally. The body is the completed response object; for a Chat Completions request, the chat completion.
  • Failed — the body is the failed response object, including error.code and error.message. This includes a background request that did not finish before its deadline: it arrives with status: "failed" and error.code: "timeout". Branch on status in your handler.

Delivery semantics

Callbacks can arrive more than once

Treat the response id in the body as an idempotency key and ignore any id you have already processed.

  • 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. If every attempt fails, the failure is logged but never affects the response record or the API. The result stays available through GET /v1/responses/{response_id} regardless of whether delivery ever succeeded.

Because delivery is best-effort, a webhook is an optimization over polling, not a replacement for having the result retrievable. Keep the retrieval path in your code as the source of truth.

Test it locally

Point a real request at a listener on your own machine.

1. Run a listener that prints the body and returns 200

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()
"

2. Tunnel to it

ngrok http 8765

3. Use the tunnel URL as completion_webhook

Paste the https://…ngrok-free.app URL from ngrok into the metadata block above and submit the request. The listener prints the payload when the response finishes.