AI Interpretation
The same assistant that interprets results in the Partner Dashboard is available over the API. There are two ways to use it, and nothing stops you using both.
| Endpoint | Use it for | |
|---|---|---|
| One-shot | POST /ai/interpretations | A written interpretation of a result, in a single request. |
| Conversation | POST /ai/conversations + messages | A back-and-forth about a patient, with follow-up questions. |
Both draw on the same material: the patient's GlycanAge results, their health questionnaire, and a curated corpus of glycan and IgG literature.
One-shot interpretation
Send a kit code, get an interpretation back:
POST /ai/interpretations
Content-Type: application/json
{
"kitCode": "GA-AB-123456"
}
{
"kitCode": "GA-AB-123456",
"patientId": "5a1c...",
"interpretation": "The glycan age of 48 sits four years above the chronological age of 44...",
"resultsConsidered": 3,
"generatedOn": 1769558460000,
"cached": false,
"sources": []
}
You don't have to assemble any context yourself. The endpoint pulls in the patient's five most recent results along with their health questionnaire, so a repeat tester gets an interpretation that understands the trend instead of reading one result in isolation. resultsConsidered tells you how many results went into the text.
interpretation is Markdown: an overview, a section per index, a trend section (left out when the patient only has one result), and considerations.
Caching
Interpretations are cached per result. The first call generates one, and every call after that for the same kit hands back the stored text.
Cached responses come with "cached": true, cost nothing, and don't count against your rate limit or monthly budget, so poll the endpoint as often as you like.
To regenerate one, say after the patient updated their health questionnaire:
{ "kitCode": "GA-AB-123456", "refresh": true }
If you'd rather not risk generating one at all, you can fetch only what's already cached:
GET /ai/interpretations/kit/GA-AB-123456
That returns 404 if nothing has been generated yet.
Steering the output
An optional instructions field, up to 2000 characters, lets you focus a single interpretation:
{
"kitCode": "GA-AB-123456",
"refresh": true,
"instructions": "The patient started a 12-week anti-inflammatory protocol in March. Focus on whether the indexes moved."
}
Tone and guidance for your whole account is set up by your GlycanAge account manager, not per request.
Conversations
A conversation is always about one patient, so open it with either a kit code or a patient ID:
POST /ai/conversations
Content-Type: application/json
{ "kitCode": "GA-AB-123456" }
{
"id": "9f2b...",
"patientId": "5a1c...",
"kitCode": "GA-AB-123456",
"title": "Patient Jane Doe",
"createdOn": 1769558460000,
"lastActivity": 1769558460000
}
Opening with kitCode pins the discussion to that result. Opening with patientId follows whichever result was released most recently. Either way, the patient's five most recent results come along as history.
The assistant already has the patient's record when the conversation opens, so you never have to repeat context on a message:
POST /ai/conversations/9f2b.../messages
Content-Type: application/json
{ "message": "How does the B index compare with their previous test?" }
{
"id": "c31d...",
"conversationId": "9f2b...",
"role": "assistant",
"content": "The bisecting GlcNAc index moved from the 71st to the 64th percentile...",
"sources": [],
"createdOn": 1769558520000
}
You can read the transcript back at any time with GET /ai/conversations/{id}/messages, list conversations with GET /ai/conversations (filter with ?patientId=), and remove one with DELETE /ai/conversations/{id}.
Streaming
Building a chat interface? Post to /messages/stream instead. The response is application/x-ndjson, one JSON object per line:
{"type":"text","content":"The bisecting GlcNAc index "}
{"type":"text","content":"moved from the 71st to the 64th percentile..."}
{"type":"complete","message":{"id":"c31d...","content":"...full reply...","sources":[]}}
There are three line types to handle:
text: appendcontentto whatever you're displaying.complete: the finished, persisted message. Treat this as the authoritative version.error: generation failed after the stream opened, and the stream ends here.
The turn is saved either way, so a dropped connection costs you nothing. Just re-read it from GET /ai/conversations/{id}/messages.
Rate limit and budget rejections happen before the stream opens, so they come back as an ordinary JSON error with an HTTP status, not as an error line.
Rate limits and budget
AI endpoints are metered per partner account.
| Control | Default | Error code when exceeded |
|---|---|---|
| Requests per minute | 10 | rate_limit_exceeded |
| Requests per day | 500 | daily_rate_limit_exceeded |
| Spend per calendar month | 100 USD | monthly_budget_exhausted |
Spend is the token cost of every model call your account makes, added up over the calendar month (UTC) and reset on the 1st. Cached interpretations are free and don't count towards it.
All three return 429 with a Retry-After header and a machine-readable code:
{
"error": "Monthly AI budget of 100.00 USD has been used. It resets at the start of next month; contact support@glycanage.com to raise it.",
"code": "monthly_budget_exhausted"
}
We can raise any of these limits for your account. Just get in touch at support@glycanage.com or ask your account manager.
Knowing where you stand
Every AI response carries the current state in its headers, so you can back off before you start getting rejected:
| Header | Meaning |
|---|---|
X-RateLimit-Limit / X-RateLimit-Remaining | Requests allowed / left in the current minute |
X-RateLimit-Limit-Day / X-RateLimit-Remaining-Day | Requests allowed / left in the trailing day |
X-AI-Budget-Limit-USD | Your monthly budget |
X-AI-Budget-Used-USD | Spend so far this month |
X-AI-Budget-Remaining-USD | What is left |
GET /ai/usage gives you the same figures plus a breakdown, and doesn't use up any quota:
{
"enabled": true,
"limits": {
"requestsPerMinute": 10,
"requestsPerDay": 500,
"monthlyBudgetUsd": 100
},
"currentMonth": {
"spendUsd": 12.4831,
"remainingUsd": 87.5169,
"requests": 214,
"interpretations": 176,
"conversationMessages": 38,
"inputTokens": 4192883,
"outputTokens": 214902
}
}
Errors
Alongside the usual error message, AI endpoints return a stable code you can branch on.
| Status | Code | Meaning |
|---|---|---|
400 | invalid_request | Missing or malformed field. |
403 | ai_disabled | AI access is not enabled for your account. |
404 | not_found | Kit, patient, conversation or interpretation does not exist, or is not yours. |
409 | result_not_ready | The kit has no released result yet. |
409 | kit_unassigned | The kit is not assigned to a patient. |
409 | no_results | The patient has no released results to discuss. |
429 | rate_limit_exceeded, daily_rate_limit_exceeded, monthly_budget_exhausted | See above. |
502 | empty_response | The model returned nothing. Retry. |
Clinical use
Responses are generated from the patient's GlycanAge data and the literature corpus. Treat them as decision support for a qualified practitioner. They aren't a diagnosis and they don't replace clinical judgement.
- Read the generated text before passing it to a patient, as you would with any generated content.
- Don't make a clinical decision on this alone.
- The assistant won't prescribe, and it frames uncertain mechanisms as hypotheses rather than fact.
Sandbox
The AI endpoints work in the sandbox against simulated results, so you can build and test the whole integration there. Sandbox usage is metered separately from production.