Skip to main content

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 -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 }}."}]}'

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 $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"

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 $ACRUXCORE_BASE_URL/prompts/a95ea910-c7fc-43c5-9c68-a8750e02a8f5/versions/1 \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"

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"}}