Prompt Versions API
All endpoints verified working via curl. Document updated only after curl
confirmation. Unlike the parent Prompts create/edit/delete
endpoints, everything here accepts a Bearer API key. Write operations (POST)
require owner, admin, or editor role.
POST /api/v1/prompts/:id/versions
Commits a new immutable version of a prompt. The prompt must already exist
(created via POST /prompts).
Version numbers are sequential
(1, 2, 3 …) per prompt. The first commit auto-creates production and staging
aliases pointing to v1; subsequent commits do not change any alias.
Optional model (issue #12): bind a default gateway model to this version by its
publicName. When set, the Playground pre-selects it and a stored-prompt gateway
call that omits model falls back to it (an explicit request model always
wins). Omitted = unbound. Every version response now carries model
(the bound model's current publicName, or null).
variables is derived from the messages, never taken from the request. It lists
every name the templates read and that the caller must supply, and it is what
the render endpoint checks before rendering. A name the template binds itself —
a {% for %} target or a {% set %} target — is not an input and is left out,
but only in the message that binds it: each message is rendered as its own
template, so a {% set company = … %} in one message does not make company any
less required in another. Committing the same messages again re-derives the list,
which is how a version committed before this rule took effect gets a corrected
one.
- curl
- TypeScript
- Python
curl -X POST $ACRUXCORE_BASE_URL/prompts/a95ea910-c7fc-43c5-9c68-a8750e02a8f5/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"system","content":"Hello {{ name }}, welcome to {{ company }}."}]}'
const version = await hub.prompts.commitVersion('a95ea910-c7fc-43c5-9c68-a8750e02a8f5', {
messages: [{ role: 'system', content: 'Hello {{ name }}, welcome to {{ company }}.' }],
});
version = await hub.prompts.commit_version(
'a95ea910-c7fc-43c5-9c68-a8750e02a8f5',
[{'role': 'system', 'content': 'Hello {{ name }}, welcome to {{ company }}.'}],
)
Response (status 201) — first commit: includes aliases:
{
"id": "1f720cc8-c817-45a1-a992-02dd2f90af64",
"promptId": "a95ea910-c7fc-43c5-9c68-a8750e02a8f5",
"versionNumber": 1,
"messages": [{"role":"system","content":"Hello {{ name }}, welcome to {{ company }}."}],
"variables": ["name","company"],
"createdBy": "...",
"createdAt": "2026-06-26T22:39:53.801Z",
"aliases": [
{"alias":"production","versionId":"1f720cc8-c817-45a1-a992-02dd2f90af64"},
{"alias":"staging","versionId":"1f720cc8-c817-45a1-a992-02dd2f90af64"}
]
}
Response (status 201) — subsequent commits: no aliases field:
{
"id": "e3d0901f-24e6-4ada-941e-5b590592d2f8",
"promptId": "a95ea910-c7fc-43c5-9c68-a8750e02a8f5",
"versionNumber": 2,
"messages": [{"role":"system","content":"Hi there {{ name }}, {{ company }} is glad you are here."}],
"variables": ["name","company"],
"createdBy": "...",
"createdAt": "2026-06-26T22:40:32.123Z",
"model": null
}
With a bound default model (issue #12) — model is the model's publicName:
curl -X POST $ACRUXCORE_BASE_URL/prompts/91f7b670-3305-47d0-af89-f46263cfabb7/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"system","content":"Hello {{ name }}"}],"model":"gpt-4o-mini"}'
{
"id": "ff24e4a5-005c-46c9-8cd1-f5715907da89",
"promptId": "91f7b670-3305-47d0-af89-f46263cfabb7",
"versionNumber": 1,
"messages": [{"role":"system","content":"Hello {{ name }}"}],
"variables": ["name"],
"createdBy": "a1ef91ce-3b55-4a70-b687-3275c1c6313f",
"createdAt": "2026-07-12T12:00:03.873Z",
"model": "gpt-4o-mini",
"aliases": [
{"id":"...","alias":"production","versionId":"ff24e4a5-005c-46c9-8cd1-f5715907da89","versionNumber":1,"updatedAt":"..."},
{"id":"...","alias":"staging","versionId":"ff24e4a5-005c-46c9-8cd1-f5715907da89","versionNumber":1,"updatedAt":"..."}
]
}
Mark a message for prompt caching
Anthropic models cache a prompt only when the request marks where the cached part ends. Add
"cache_control": {"type": "ephemeral"} to a message, and the gateway caches the prompt up to
and including that message on any Anthropic model, whether the connection is Anthropic itself
or OpenRouter. content stays a plain string. Every other model ignores the field, so one
version still runs against any model in an experiment. OpenAI and Gemini cache long prompts
without a marker. See prompt caching on the gateway
for what a cached call returns and costs.
version.json holds a 22,795-character product catalogue as the system message (shortened
below), marked for caching, and a short user template:
curl -X POST $ACRUXCORE_BASE_URL/prompts/3c449918-002f-4079-b359-e3294a66fab1/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d @version.json
{
"id": "d6db91b2-8ed3-4226-a637-4b30ac1dce65",
"promptId": "3c449918-002f-4079-b359-e3294a66fab1",
"versionNumber": 1,
"messages": [
{
"role": "system",
"content": "You are the support agent for Acme Lamps. Answer only from this catalogue.\n\n- SKU 1000: …",
"cache_control": {"type": "ephemeral"}
},
{"role": "user", "content": "{{ question }}"}
],
"variables": ["question"],
"createdBy": "18c76b52-ee0d-4001-be8e-c29976488fbb",
"createdAt": "2026-10-10T09:16:09.531Z",
"model": "claude-haiku",
"aliases": [
{"id":"792d43f6-f828-444f-a839-6927557750aa","alias":"production","versionId":"d6db91b2-8ed3-4226-a637-4b30ac1dce65","versionNumber":1,"updatedAt":"2026-10-10T09:16:09.535Z"},
{"id":"174339cf-9e53-4a0d-9641-fb1815a0957c","alias":"staging","versionId":"d6db91b2-8ed3-4226-a637-4b30ac1dce65","versionNumber":1,"updatedAt":"2026-10-10T09:16:09.535Z"}
]
}
400 — cache_control.type is anything other than ephemeral:
{"error":{"code":"VALIDATION_ERROR","message":"cache_control.type must be 'ephemeral'"}}
400 — model is not a registered gateway model for the team (issue #12):
{"error":{"code":"VALIDATION_ERROR","message":"Model 'nope-model' is not registered for this team."}}
400 — invalid nunjucks template syntax:
{"error":{"code":"TEMPLATE_PARSE_ERROR","message":"..."}}
400 — missing or invalid messages field:
{"error":{"code":"VALIDATION_ERROR","message":"..."}}
403 — viewer role cannot commit versions:
{"error":{"code":"FORBIDDEN","message":"You do not have permission to perform this action."}}
404 — prompt does not exist:
{"error":{"code":"NOT_FOUND","message":"Prompt not found"}}
GET /api/v1/prompts/:id/versions
Lists all versions for a prompt, newest first. The messages field is omitted
from list items (fetch a specific version to get messages).
- curl
- TypeScript
- Python
curl $ACRUXCORE_BASE_URL/prompts/a95ea910-c7fc-43c5-9c68-a8750e02a8f5/versions \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"
With pagination:
curl "$ACRUXCORE_BASE_URL/prompts/a95ea910-c7fc-43c5-9c68-a8750e02a8f5/versions?page=1&limit=20" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"
const result = await hub.prompts.listVersions('a95ea910-c7fc-43c5-9c68-a8750e02a8f5');
// with pagination
const page = await hub.prompts.listVersions('a95ea910-c7fc-43c5-9c68-a8750e02a8f5', { page: 1, limit: 20 });
result = await hub.prompts.list_versions('a95ea910-c7fc-43c5-9c68-a8750e02a8f5')
# with pagination
page = await hub.prompts.list_versions('a95ea910-c7fc-43c5-9c68-a8750e02a8f5', page=1, limit=20)
Response (status 200):
{
"data": [
{
"id": "e3d0901f-24e6-4ada-941e-5b590592d2f8",
"promptId": "a95ea910-c7fc-43c5-9c68-a8750e02a8f5",
"versionNumber": 2,
"variables": ["name","company"],
"createdBy": "...",
"createdAt": "2026-06-26T22:40:32.123Z"
},
{
"id": "1f720cc8-c817-45a1-a992-02dd2f90af64",
"promptId": "a95ea910-c7fc-43c5-9c68-a8750e02a8f5",
"versionNumber": 1,
"variables": ["name","company"],
"createdBy": "...",
"createdAt": "2026-06-26T22:39:53.801Z"
}
],
"total": 2,
"page": 1,
"limit": 20
}
GET /api/v1/prompts/:id/versions/:version_number
Fetches a specific version including its full messages array.
- curl
- TypeScript
- Python
curl $ACRUXCORE_BASE_URL/prompts/a95ea910-c7fc-43c5-9c68-a8750e02a8f5/versions/1 \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"
const version = await hub.prompts.getVersion('a95ea910-c7fc-43c5-9c68-a8750e02a8f5', 1);
version = await hub.prompts.get_version('a95ea910-c7fc-43c5-9c68-a8750e02a8f5', 1)
Response (status 200):
{
"id": "1f720cc8-c817-45a1-a992-02dd2f90af64",
"promptId": "a95ea910-c7fc-43c5-9c68-a8750e02a8f5",
"versionNumber": 1,
"messages": [{"role":"system","content":"Hello {{ name }}, welcome to {{ company }}."}],
"variables": ["name","company"],
"createdBy": "...",
"createdAt": "2026-06-26T22:39:53.801Z"
}
404 — version number does not exist for this prompt:
{"error":{"code":"NOT_FOUND","message":"Version 99 not found for this prompt"}}