Callbacks
Scoring is asynchronous. When an evaluation completes, Vindex POSTs the full score record to the callback URL you registered so you can store it, render it in your own UI, or drop it back into Jira.
When callbacks fire
A callback is delivered once per successful evaluation. If scoring fails at the model layer, Vindex still POSTs a callback but with status: “failed” and an error message. Your webhook should accept both cases.
The target URL is the one Vindex generated for you at registration. You can override it per request by supplying a webhook.url field on the evaluate call.
Payload
(your webhook URL){
"job_id": "b1f9...",
"customer_id": "acme_jira_8f2c",
"ticket": {
"id": "PROJ-101",
"key": "PROJ-101",
"title": "As a site admin I want to export user activity",
"snapshot_at": "2026-04-16T09:12:03Z"
},
"invest_scores": {
"independent": 6,
"negotiable": 5,
"valuable": 7,
"estimable": 4,
"small": 5,
"testable": 4
},
"overall_score": 5.2,
"top_recommendations": [
"Add acceptance criteria that describe the observable outcome.",
"Split the export into per-entity stories to reduce scope."
],
"context_used": {
"testable": [ "No acceptance criteria section present" ],
"small": [ "Description references four data surfaces" ]
},
"model_metadata": {
"provider": "openai",
"model_name": "gpt-4o"
},
"processing_metadata": {
"started_at": "2026-04-16T09:12:03Z",
"completed_at": "2026-04-16T09:12:08Z",
"duration_ms": 4821.4
},
"created_at": "2026-04-16T09:12:08Z"
}The model temperature is never returned in callback payloads or API responses. Everything else in the stored score document is included.
Signature verification
Every callback is signed with the shared secret you supplied at registration. Verify the signature before trusting the payload.
X-Vindex-Signature: sha256=8e4b2f...
X-Vindex-Timestamp: 1734349928
X-Vindex-Job-Id: b1f9...payload = raw_request_body
timestamp = header["X-Vindex-Timestamp"]
expected = hmac_sha256(shared_secret, timestamp + "." + payload)
received = header["X-Vindex-Signature"].split("=")[1]
if not constant_time_equals(expected, received):
reject()
if now - int(timestamp) > 300: # 5-minute replay window
reject()Your response and retry behaviour
- Return
2xxwithin 10 seconds to acknowledge delivery. - Any
4xxother than408cancels retries — Vindex assumes the callback is permanently rejected. 5xx,408, and network errors are retried with exponential backoff for up to 6 attempts over 24 hours.- The delivery status —
delivered,failed, orpending— is stored on the score record alongside the HTTP status code, retry count, response time, and any error message.
Idempotency
Retries carry the same job_id. Treat that field as the idempotency key — if you've already processed a payload with that job_id, respond 200 OK without re-running your side effects.
