One endpoint.
Every answer. ask.hailey.io
An OpenAI-compatible AI gateway. Point any OpenAI-style client at Hailey, authenticate with your service key, and get streaming or complete responses — with durable per-project context no vanilla endpoint gives you.
Quickstart
Three lines of setup in any OpenAI client — only the base URL changes.
curl https://ask.hailey.io/v1/chat/completions \
-H "Authorization: Bearer $HAILEY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-4-8",
"messages": [{"role": "user", "content": "Hello, Hailey."}]
}'from openai import OpenAI
client = OpenAI(
base_url="https://ask.hailey.io/v1",
api_key="hly_…",
)
resp = client.chat.completions.create(
model="claude-opus-4-8",
messages=[{"role": "user", "content": "Hello, Hailey."}],
)
print(resp.choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://ask.hailey.io/v1",
apiKey: process.env.HAILEY_API_KEY,
});
const resp = await client.chat.completions.create({
model: "claude-opus-4-8",
messages: [{ role: "user", content: "Hello, Hailey." }],
});
console.log(resp.choices[0].message.content);Authentication
Every request needs a service key in the Authorization header:
Authorization: Bearer hly_…. Keys are created per integration on the
API keys page, are shown once at creation, and can
be revoked at any time.
Each key can carry a default project — the context it runs in when a request doesn't name one (see Projects).
Models & engines
GET /v1/models lists the models your
key can use. A model id pins the engine (the AI backend adapter),
the model family, and the version in one string —
pick the id, and the gateway routes to the right backend.
| Model id | Engine | Context window | Max output |
|---|---|---|---|
claude-opus-4-8 |
anthropic | 200,000 | 32,000 |
claude-opus-4-7 |
anthropic | 200,000 | 32,000 |
claude-sonnet-4-6 |
anthropic | 200,000 | 64,000 |
claude-haiku-4-5 |
anthropic | 200,000 | 8,192 |
claude-fable-5 |
anthropic | 200,000 | 64,000 |
Additional engines (e.g. OpenAI-family models) appear in this list as adapters are
enabled — your integration code doesn't change, you just select a different model id.
Calling a registered model whose engine isn't enabled yet returns
501 engine_not_available. If a request omits model, the
project's default model is used.
Chat completions
POST /v1/chat/completions — the OpenAI chat-completions shape. Minimal request:
{
"model": "claude-opus-4-8",
"messages": [
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "What is Hailey?"}
]
}{
"id": "chatcmpl-…",
"object": "chat.completion",
"model": "claude-opus-4-8",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "…" },
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 123, "completion_tokens": 45, "total_tokens": 168 }
}
Send the full messages history each call, exactly as with OpenAI —
prior user/assistant turns are treated as conversation
context. system messages are appended to the project's own system
prompt (project instructions always apply first).
Image input
A user message may carry images using the OpenAI content-array form.
Send each image as a base64 data: URI in an image_url part:
{
"model": "claude-opus-4-8",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What does the label on this jar say?"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ…"}}
]
}]
}
JPEG, PNG, GIF and WebP; up to 5 MB per image, 8 images and 20 MB per request,
8000 px per side. The type is read from the image itself, not from the URI.
Remote http(s) image URLs are not fetched — encode the image and send it.
Images work with streaming and with response_format, and may sit in any
user turn of the history. An image that cannot be accepted is rejected
with a 400; it is never silently dropped.
Streaming
Set "stream": true to receive server-sent events of
chat.completion.chunk objects, terminated by data: [DONE] —
the standard OpenAI streaming contract, so SDK streaming helpers work unchanged.
The final chunk carries usage.
stream = client.chat.completions.create(model="…", messages=[…], stream=True)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")Projects — durable context
A plain gateway call is stateless: it runs in a fresh, empty sandbox. A project gives your calls durable context — the gateway equivalent of running inside your project directory.
404 project_not_found.
| What a project carries | Effect on every call |
|---|---|
| System prompt | Always-on instructions, applied before any request system message. |
CLAUDE.md | A project brief placed at the workspace root and auto-loaded by the engine. |
| Context files | Seeded into the call's workspace — the model can read them while answering. |
| Defaults | Model (engine + version), parameters, tool allowlist, workspace mode. |
Create and manage projects at /settings/projects.
Bind one to a key as its default, or select per request with
hailey.project. Keys without a project use an auto-provisioned personal
default project. A key can only reach projects owned by the same account.
Workspace modes
Calls run in parallel by default — including calls on the same key, project,
and account. Fire as many concurrent requests as you need; nothing on the server
queues them. Sequential execution is opt-in: pass
"hailey": { "params": { "serial": true } } and calls to that project
queue and wait their turn — each request blocks until the one ahead of it
finishes, then runs. You only see 409 project_busy if a call waits
longer than the server's queue cap (minutes) — an exceptional case, not the norm.
conversation default · parallel
Each call gets a fresh workspace seeded from the project's context files.
File mutations are discarded after the call. Fully parallel — serialize with
params.serial when order matters.
shared always serial
Calls run inside the project's persistent workspace — files written by one call are visible to the next. Two engines can't safely share one live workspace, so calls queue per project.
Conversations (shared mode)
In shared mode you may pass a conversation id of your
choosing. The gateway maps it to a persistent engine session and resumes it on every
call with the same id — server-side memory without re-sending history:
"hailey": { "workspace": "shared", "conversation": "onboarding-7c2" }The hailey extension & headers
Hailey-specific options ride in an optional hailey object in the request
body (use extra_body in the OpenAI SDKs). Clients that can't add body
fields can send the equivalent headers.
{
"model": "claude-opus-4-8",
"messages": [ … ],
"hailey": {
"project": "birthday-gold", // project slug or id (default: the key's project)
"workspace": "shared", // 'conversation' (default) | 'shared'
"conversation": "onboard-7c2", // shared mode only: resume this session
"params": { "max_turns": 8 },// per-call engine parameter overrides
"retain_days": -1 // -1 = keep no transcript; 1–365 = delete after N days
}
}X-Hailey-Project: birthday-gold
X-Hailey-Workspace: shared
X-Hailey-Conversation: onboard-7c2
X-Hailey-Retain-Days: -1client.chat.completions.create(
model="claude-opus-4-8",
messages=[{"role": "user", "content": "Summarize the seed data."}],
extra_body={"hailey": {"project": "birthday-gold"}},
)Actions — named tasks
An action is a task Hailey defines once and every site calls by name: the prompt, the
output schema, the model and any scoring live in Hailey, not in your code. Fix or improve it
once and every caller gets the change. An action must be granted to your account; one you
can't use answers 404 action_not_found, the same as one that doesn't exist.
curl https://ask.hailey.io/v1/actions -H "Authorization: Bearer $HAILEY_API_KEY" # what you may call
curl https://ask.hailey.io/v1/actions/kyc-verify -H "Authorization: Bearer $HAILEY_API_KEY" # its inputs + images
curl https://ask.hailey.io/v1/actions/kyc-verify -H "Authorization: Bearer $HAILEY_API_KEY" \
-H "Content-Type: application/json" -d '{
"inputs": { "name_on_account": "Jane Doe", "postal_code": "80202", "challenge": "Give a thumbs-up next to your face" },
"images": { "id_front": "data:image/jpeg;base64,…", "id_back": "data:image/jpeg;base64,…", "selfie": "data:image/jpeg;base64,…" }
}'{
"object": "action.result", "id": "act_…", "action": "kyc-verify", "version": 1, "model": "claude-opus-4-8",
"verdict": "review", // pass | review | fail — act on this
"score": 76, // 0–100, in the verdict's band: pass 90–100, review 31–89, fail 0–30
"components": { "document": 100, "name": 100, "face": 30, "liveness": 100 },
"reasons": [ { "code": "face_not_assessed", "severity": "review", "message": "The automatic face comparison was not made." } ],
"address_match": "match", // match | different | unread | not_checked — never decisive
"extracted": { "name_on_id": "…", "expires_on": "2030-01-01", "document_number_last4": "1234", … },
"checks": { … the model's raw answer … },
"retention": "none", // standard | none | Nd — see retain_days
"usage": { "prompt_tokens": 15469, "completion_tokens": 844, "total_tokens": 16313 }
}| Action | What it does |
|---|---|
kyc-verify | Reads a government photo ID, compares its name with the account name and its portrait with a live selfie, checks liveness and the requested gesture. fail: not an ID, expired, a different name, tampering or a sample card, or a high-confidence different face. pass: every check clear-cut. Anything else is review — have a person look. Never retained: no engine transcript, whatever retain_days says, and Hailey logs only verdict, score and timing. |
502 means the action couldn't finish (engine error or no structured answer): retry later, or send the case to a person.Parameters
Parameters resolve in three layers: project defaults → per-request
hailey.params → standard OpenAI top-level fields. Support depends on the
engine; unsupported parameters are accepted and ignored, never fatal.
| Parameter | Where | Support | Behavior (anthropic engine) |
|---|---|---|---|
model | top level | honored | Selects engine, family, and version. |
stream | top level | honored | SSE streaming on/off. |
messages (system) | top level | honored | Appended to the project system prompt. |
max_turns | hailey.params / project | honored | Caps agentic tool-use turns (1–50, default 20). |
max_thinking_tokens | hailey.params / project | honored | Extended-thinking budget (0–32000). Default 0: thinking off for the fastest answers; raise it for harder reasoning. |
retain_days | hailey.retain_days / top level / X-Hailey-Retain-Days; actions: top level | honored | Transcript retention for this call. Omitted: standard retention (a full transcript is kept; see Privacy). -1: no transcript is written. 1–365: the transcript is deleted after that many days. The response echoes the setting in the X-Hailey-Retention header (standard | none | Nd), or as retention on action results. You can't combine it with hailey.conversation. Actions marked never-retained (kyc-verify) answer none whatever you send. |
serial | hailey.params / project | honored | Default false: calls run fully in parallel, even within one account. Set true and calls queue per project, each waiting for the previous one to finish. |
| tool allowlist | project setting | honored | Restricts which tools the engine may use. Code-execution and file-mutation tools are always disabled on gateway calls. |
temperature, top_p, max_tokens | top level | ignored | Accepted for compatibility; the engine manages sampling and output budget. |
Errors
Errors use the OpenAI error envelope:
{ "error": { "type": "authentication_error", "message": "Invalid API key.", "code": null } }| Status | Code | Meaning |
|---|---|---|
| 401 | — | Missing or invalid Authorization: Bearer hly_…. |
| 400 | invalid_workspace_mode, conversation_requires_shared, … | Malformed request. |
| 400 | image_url_not_supported, invalid_image, unsupported_image_type, image_too_large, too_many_images | An image part was refused: a remote URL instead of a data URI, bad base64, not a readable JPEG/PNG/GIF/WebP, over a size limit, or more than 8 images. |
| 400 | invalid_retain_days, retention_conflict | retain_days isn't -1 or 1–365, or it was sent with a conversation, which needs its transcript to resume. |
| 404 | project_not_found | Project unknown to this account (or archived). |
| 400 | invalid_action_input | An action input or image is missing, unknown, the wrong type or too long. |
| 404 | action_not_found | No such action, or it isn't granted to your account. |
| 404 | model_not_found | Unknown or disabled model id. |
| 409 | project_busy | A serialized call (shared mode or params.serial) waited longer than the queue cap. Exceptional — serialized calls normally queue and complete. Retry with backoff. |
| 501 | engine_not_available | The model's engine has no adapter enabled yet. |
| 502 | upstream_error | The engine failed mid-call. Safe to retry. |
| 503 | engine_unavailable | The engine is temporarily unavailable (e.g. mid-update). Retry shortly. |
Ready to build?
Request access, create a key, give it a project — and every call arrives briefed.
Request access Create a key See the live demo