Saved prompt library
Save the prompts you reuse the most, version them, render them with variables, and reference them from anywhere in your application without redeploying client code every time you tweak the wording.
Quickstart
# 1. Save a prompt with two variables
curl -X POST https://app.privaterouter.com/api/prompts \
-H "Authorization: Bearer $PR_JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "Summarise email",
"description": "Concise 3-bullet summary of an inbound message",
"content": "Summarise the following email from {{ sender }} in 3 bullet points. Tone: {{ tone }}.\n\n---\n{{ body }}",
"variables": [
{"name": "sender", "description": "who sent the email"},
{"name": "tone", "default": "professional"},
{"name": "body", "description": "raw email body"}
]
}'
# → 201, returns {id, slug:"summarise-email", active_version:{version:1, ...}}
# 2. Render it with variables
curl -X POST https://app.privaterouter.com/api/prompts/summarise-email/render \
-H "Authorization: Bearer $PR_JWT" \
-H "Content-Type: application/json" \
-d '{
"variables": {
"sender": "alice@example.com",
"body": "Lorem ipsum..."
}
}'
# → 200 {"rendered_content": "Summarise the following email from alice@example.com...",
# "version_used": 1}
Why use this
- Decouple prompts from code. Iterate on wording in the dashboard; your application code keeps referencing the same slug.
- Version history. Every change is a new immutable version. Roll back instantly by activating an older one.
- Variables. Mark the spots that change per-call with
{{ variable }}placeholders. Declare defaults so callers can omit them when they want. - Per-user namespace. Slugs are unique within your account. There is no global registry.
{{ variable }} syntax
Placeholders use a single {{ name }} form. Whitespace inside the
braces is optional. Names must match ^[A-Za-z_][A-Za-z0-9_]*$.
Hello {{ name }}, your balance is {{balance}}.
Render-time rules:
- Values supplied in the render request take priority.
- If a value is missing, the variable's declared
defaultis used. - If neither is available, the render fails with
400and lists every missing required variable.
Versioning model
- Creating a prompt also creates version 1, which is set active.
POST /api/prompts/{id}/versionsappends a new version but does not auto-promote it. Drafts stay invisible until you flip the active pointer.POST /api/prompts/{id}/versions/{vid}/activateflips the active pointer to a specific version.- Versions are append-only history. There is no edit-in-place.
Routes
| Method | Path | Purpose |
|---|---|---|
GET | /api/prompts | List your prompts (with active version) |
POST | /api/prompts | Create prompt + initial version |
GET | /api/prompts/{id_or_slug} | Detail (with full version history) |
PATCH | /api/prompts/{id} | Update name / description / active_version_id |
DELETE | /api/prompts/{id} | Hard delete |
POST | /api/prompts/{id}/versions | Add a version (no promote) |
POST | /api/prompts/{id}/versions/{vid}/activate | Promote a version |
POST | /api/prompts/{id}/render | Substitute variables, return text |
All routes require a logged-in user; cross-user access returns 404.
Using rendered prompts in API requests
For now, the integration with chat-completions is manual: call the
/render endpoint to get the substituted text, then paste it into
your messages[0].content when calling /v1/chat/completions:
import httpx, os
JWT = os.environ["PR_JWT"]
API = "https://app.privaterouter.com"
# 1. Render the prompt
rendered = httpx.post(
f"{API}/api/prompts/summarise-email/render",
headers={"Authorization": f"Bearer {JWT}"},
json={"variables": {"sender": "alice@example.com", "body": "..."}},
).json()["rendered_content"]
# 2. Send to chat-completions
resp = httpx.post(
f"{API}/v1/chat/completions",
headers={"Authorization": f"Bearer sk-..."}, # your API key
json={
"model": "privaterouter/fast",
"messages": [{"role": "user", "content": rendered}],
},
)
Auto-using a saved prompt slug inside /v1/chat/completions directly
(e.g. {"prompt_slug": "summarise-email", "prompt_variables": {...}})
is a planned follow-up — for the M18 release the render endpoint is
the integration point.
Tips
- Use descriptive variable names.
{{ customer_email }}is much easier to read in version diffs than{{ x }}. - Always set defaults for optional knobs (tone, length, formatting). It keeps callers simple and lets you change behaviour without breaking integrations.
- Treat versions like git commits. Use the
notesfield to record what changed — future you will appreciate it during a rollback.