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.
| Key | Type | Meaning |
|---|---|---|
metadata.completion_webhook | string | Destination URL for the callback. Must be http or https. If omitted or unusable, no callback is sent and the request is unaffected. |
metadata.webhook_token | string | Optional 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.
| Part | Value |
|---|---|
| Method | POST to the URL in completion_webhook |
Content-Type | application/json |
Authorization | Present only when webhook_token was set: Bearer <webhook_token> |
| Body | The 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.codeanderror.message. This includes a background request that did not finish before its deadline: it arrives withstatus: "failed"anderror.code: "timeout". Branch onstatusin 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 87653. 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.
Max queue wait
Bound how long a request may wait behind queued work, and use the bound to fail over to another provider when sference is congested.
Decisions
Realtime classification with POST /v1/decisions — ask up to 64 choice, score, or yes/no questions about one input and get calibrated probabilities back in a single call.