API reference
The Vindex HTTP API covers registration, ticket evaluation, score callbacks, and aggregate stats. It's the same API the Jira Forge app uses; build directly on it to wire story health scoring into whatever tooling your team already relies on.
Base URL and conventions
https://edge.vindexgateway.com- JSON only. All requests and responses use
application/json, except file uploads which usemultipart/form-data. - UTC timestamps. All dates are ISO 8601 in UTC.
- Customer ID scoping. Every ticket you submit is tagged with the
customer_idyou were assigned when your integration was provisioned. Scores are only visible to that customer and to your linked company. - Standard HTTP codes.
2xxfor success,4xxfor client errors,5xxfor server errors. Error bodies include adetailfield describing what went wrong.
Authentication
Authenticated endpoints take an API key as a bearer token. A Client Admin creates keys in the Vindex portal under API keys(a Vindex Admin can do it from the company page), names each one for the agent or system that will use it, and can show or revoke it at any time. The same key works for Vindex MCP when an agent can't sign in with OAuth. Pass it on every request:
Authorization: Bearer <your_api_key>The /evaluate/* endpoints that power the public CSV flow are unauthenticated but rate-limited per IP; see the evaluate reference for limits.
Synchronous vs. asynchronous scoring
Vindex exposes the scoring engine in two shapes. Pick whichever fits how your integration is wired; both run the identical INVEST analysis and return the same dimensions.
Synchronous
POST /api/v1/score blocks until scoring finishes and returns the full score in the response body. Best for request/response contexts: CI checks, scripts, and the CSV evaluator. No callback endpoint required.
Asynchronous
POST /api/v1/evaluate/{source} acknowledges immediately with 202 and delivers the completed score to your webhook. This is what the Jira Forge app uses.
See the evaluate reference for full request and response payloads for both models.
Endpoint groups
Evaluate
Submit a ticket (or a CSV of tickets) for INVEST evaluation. The scoring engine runs asynchronously and delivers results via callback.
Open referenceCallbacks
Receive completed scores at your own webhook endpoint. Verify the signature, handle retries, and store results in your own system.
Open referenceStats
Read aggregate story health data (averages, trends, and per-dimension breakdowns) programmatically.
Open referenceRate limits
| Endpoint group | Limit | Window |
|---|---|---|
| /api/v1/score | Monthly story quota (new unique stories) | per calendar month (UTC) |
| /evaluate/upload | 10 requests | 1 hour (per IP) |
| /evaluate/score | 5 requests | 1 hour (per IP) |
The monthly quota counts new unique stories: re-scoring a story that has already been counted is free forever and is admitted even when the quota is exhausted. When a new story is refused, the response is 429 with code: "QUOTA_EXHAUSTED" and a machine-readable body:
{
"error": "Quota exhausted",
"code": "QUOTA_EXHAUSTED",
"message": "The monthly story quota is reached. New stories are paused until the quota resets; previously scored stories can still be re-scored at no cost.",
"plan": "starter",
"quota": 200,
"used": 200,
"reset_at": "2026-09-01T00:00:00Z",
"upgrade_url": "https://register.vindexgateway.com/portal",
"already_counted": false
}Per-IP limits on the public CSV endpoints return 429 Too Many Requests with a Retry-After header. Distinguish quota exhaustion by the code field, never by the status alone.
Story identity & billing
Billing is per unique story, once. Send a stable external_id with each request (your own record id) and every later score of that id is a free re-score, in any month. Without an external_id, Vindex falls back to a normalized fingerprint of the story's content: unchanged content stays free, edited content bills as a new story. Responses disclose the applied rule in identity_resolution (external_id or content_hash) and echo the resolved story_identity, so the billing unit is always visible at the point of use.
Error shape
Statistics endpoints (/stats/*) return every error in one envelope, with nothing else in the body:
{
"error": {
"code": "DATE_RANGE_TOO_LONG",
"message": "The date range cannot be longer than 365 days.",
"request_id": "req_2f9c1a7e4b8d4c0f9a3e6d5b7c1f2a3b"
}
}Branch on code, never on message: codes are stable, and each always comes with the same HTTP status. The same request_idis returned in theX-Request-Idheader on every response. Include it when you contact support.
| Status | Code | Meaning |
|---|---|---|
| 400 | IDENTIFIER_INVALID | The issue or project key is not in a valid format for your platform. Treat it as not found. |
| 401 | UNAUTHENTICATED | No valid credentials were supplied. |
| 403 | ACCESS_DENIED | The credentials are valid but do not permit this tenant or resource. |
| 404 | NOT_FOUND | The resource does not exist. |
| 422 | DATE_RANGE_INCOMPLETE | Only one of from or to was sent. Send both, or neither for the last 30 days. |
| 422 | DATE_RANGE_INVALID | from is after to. |
| 422 | DATE_RANGE_TOO_LONG | The range is longer than 365 days. |
| 422 | DATE_FORMAT_INVALID | A date is not in YYYY-MM-DD format. |
| 422 | PAGINATION_INVALID | page is below 1, or pageSize is outside 1 to 200. |
| 422 | FILTER_INVALID | A filter value, such as the activity status, is not supported. |
| 429 | RATE_LIMITED | Too many requests. Wait for the number of seconds in the Retry-After header. |
| 500 | INTERNAL_ERROR | An unexpected error. Quote the request_id when reporting it. |
Scoring endpoints (/api/v1/*) still use their earlier error body:
{
"error": "Validation failed",
"message": "Invalid request format",
"details": ["Missing required fields: ticket_id or ticket_data"]
}The unauthenticated CSV endpoints (/evaluate/*) are served directly by the scoring backend and use the framework default shape instead:
{
"detail": "CSV must have at least 2 columns"
}