AI Interpretation
The GlycanAge assistant 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 kit, with follow-up questions. |
Both draw on the same material: the kit's released result, the age, sex and ethnicity recorded with it, the health questionnaire your customer filled in when registering, and a curated corpus of glycan and IgG literature.
Your customers register their own kits and aren't modelled as patients, so a kit and its single result are the whole subject. There's no result history to compare against and no way to ask about a person across several tests. If a customer buys twice, that's two unrelated kits. We tell the assistant this explicitly, so it won't invent a trend.
Responses are written to your customer, in the second person, in the same voice as the consumer report they get. You can drop the text straight into your own product: there's no GlycanAge branding, no greeting, no sign-off, and it never mentions you.
One-shot interpretation
Send a kit code, get an interpretation back:
POST /ai/interpretations
Content-Type: application/json
{
"kit_code": "GA-AB-123456"
}
{
"kit_code": "GA-AB-123456",
"interpretation": "Your glycan age of 48 sits four years above your chronological age of 44...",
"generated_on": 1769558460000,
"cached": false,
"sources": []
}
interpretation is Markdown: an overview, a section per index, and considerations. sources lists any scientific literature the assistant cited.
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 as often as you like.
To regenerate one, say after the customer updated their health questionnaire:
{ "kit_code": "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, which is handy if you only want to show an interpretation when one already exists.
Steering the output
An optional instructions field, up to 2000 characters, lets you shape a single interpretation, say to cap its length or match your house style:
{
"kit_code": "GA-AB-123456",
"refresh": true,
"instructions": "Keep it under 300 words and avoid section headings."
}
Conversations
A conversation is always about one kit:
POST /ai/conversations
Content-Type: application/json
{ "kit_code": "GA-AB-123456" }
{
"id": "9f2b...",
"kit_code": "GA-AB-123456",
"title": "Kit GA-AB-123456",
"created_on": 1769558460000,
"last_activity": 1769558460000
}
The assistant already has the result when the conversation opens, so you never have to repeat context on a message:
POST /ai/conversations/9f2b.../messages
Content-Type: application/json
{ "message": "What does my B index mean?" }
{
"id": "c31d...",
"conversation_id": "9f2b...",
"role": "assistant",
"content": "Your bisecting GlcNAc index sits at the 64th percentile...",
"sources": [],
"created_on": 1769558520000
}
You can read the transcript back at any time with GET /ai/conversations/{id}/messages, list conversations with GET /ai/conversations (filter with ?kit_code=), 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":"Your bisecting GlcNAc index "}
{"type":"text","content":"sits at 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 reseller 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.
We can raise any of these limits for your account. Just email 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": {
"requests_per_minute": 10,
"requests_per_day": 500,
"monthly_budget_usd": 100
},
"current_month": {
"spend_usd": 12.4831,
"remaining_usd": 87.5169,
"requests": 214,
"interpretations": 176,
"conversation_messages": 38,
"input_tokens": 4192883,
"output_tokens": 214902
}
}
Errors
AI endpoints use the same error envelope as the rest of the API, with 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, conversation or interpretation does not exist, or is not yours. |
409 | result_not_ready | The kit has no released result yet. |
409 | no_results | The kit's result is no longer available. |
429 | rate_limit_exceeded, daily_rate_limit_exceeded, monthly_budget_exhausted | See above. |
502 | empty_response | The model returned nothing. Retry. |
Wait for result.ready before calling either endpoint. Until then, every request comes back as a 409 result_not_ready.
Responsible use
Responses are generated from your customer's GlycanAge result and the literature corpus. Treat them as general wellness information. They aren't medical advice, they aren't a diagnosis, and they don't replace a qualified professional.
- Read the generated text before putting it in front of a customer, as you would with any generated content.
- Present it as an interpretation of the test, not as guidance from you.
- 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 thing there. Sandbox usage is metered separately from production.
Do keep in mind that sandbox result values are random. The text will read fine, but the clinical picture it describes is meaningless, so use the sandbox to test your plumbing rather than your copy.