Overview
Register agents, write their prompt files, and let a running deployment read them back on every call.
One deployment can answer calls for many agents from a single codebase. This API is how a partner registers each of those agents with Subcore and stores the prompt files it should answer with. The deployment reads that configuration back on every call, so a prompt edit is live on the next one.
There are two surfaces:
| Surface | Who calls it | What it does |
|---|---|---|
/agents | You, from your own tooling | Create or update an agent, and list, write or remove its prompt files |
/agent-config/{routingKey} | Your deployment, on every call | Returns one agent's settings and files. Authenticated with the deployment's own key |
Base URL: https://api.subcore.ai
Authentication
Every request carries a Subcore API key as a bearer token, the same way the telemetry read API does:
Authorization: Bearer sk_live_...
Content-Type: application/jsonThe key resolves to one organization, and every query is scoped to it. An agent that belongs to another organization gets the same answer as one that does not exist.
The manage-agents permission
The /agents routes need a key whose permissions.manageAgents is exactly
true. You grant it yourself: an organization admin ticks Manage agents on
the key in the Subcore console, under Settings → API keys. No key has it by
default, and a key issued before the permission existed does not gain it, so an
existing key needs the box ticked rather than being replaced. A key without it
authenticates and is then refused:
{
"error": "This API key cannot manage agents: it lacks the manageAgents scope",
"code": "FORBIDDEN",
"timestamp": 1789569967913
}The key your deployment runs with is a different key. It is issued from the deployment's own page in the Subcore console, it reads configuration and sends telemetry, and it never carries the manage-agents permission. See connecting your deployment.
Conventions
Every answer is Cache-Control: no-store. A response body here can be a
prompt, and an edit is live on the next call, so nothing between you and
Subcore may keep a copy.
Every refusal has the same shape, the envelope used across api.subcore.ai.
Switch on code; the sentence in error may change.
{ "error": "No agent with this slug", "code": "NOT_FOUND", "timestamp": 1789569967913 }The full list of codes is under errors.
An agent is a core. The slug here is the same slug the telemetry API
reports as core_slug and filters by as coreSlug, and the same one a key's
permissions.cores names. It is the name you choose, and it appears in every
/agents URL. The routing key is minted by Subcore when the agent is created
and never changes. It is the agent's address on your deployment.
What makes a deployment answer for an agent
Nothing on an agent names a deployment, and no route here has a field for one. An agent is bound to a deployment by two things and no third: the organization it was created in, and the address a call arrives on.
Your deployment answers {tenantPath}/… for any routing key it is given, reads
that agent's configuration with its own deployment
key, and serves the call with what comes back. So it answers for every agent in
its own organization whose configuration that key can read. Creating the agent
with the right key is the whole binding, and there is nothing to switch on
afterwards.
A deployment key issued in the console reads all of them. Its cores permission
decides whether it may read agents at all, and reading is on unless someone
turns it off; there is no setting that leaves it some agents and not others.
Turn it off and the config read answers 403 FORBIDDEN for every agent, which
is why a key that reads one agent reads the rest.
An agent still on repo is answered with the deployment's own files, so it
works from the moment it exists. Once you move one to database, one more
thing has to line up, and it is not a binding either: the files have to sit
under a directory the deployment serves,
or the call fails at the deployment rather than falling back.
Rate limits
The /agents routes allow 120 requests per minute per key. The count is per
key once a request has authenticated, and per client address before that.
Every answer carries the count:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the window |
X-RateLimit-Remaining | Requests left in this window |
Retry-After | Seconds until the window resets. Sent on a 429 only |
Over budget, you get a 429 with code: "RATE_LIMITED". The
config read your deployment makes is not rate
limited, because a refusal there would fail a live call.
Setting up an agent
The order matters. Two guards stop an agent from reading its prompts from the database while it has none stored, so the files go in before the switch.
Create the agent
PUT /agents/{slug} with its name, timezone and phone numbers. A new agent
starts on instructionSource: "repo" and answers with the shared files in
your deployment. The answer carries its routingKey and tenantPath. See
agents.
Write its files
PUT /agents/{slug}/instructions/{path} for each prompt file. See
instructions.
Switch it to the database
PUT /agents/{slug} with { "instructionSource": "database" }. From the next
call on, the files you stored are laid over the deployment's own by path: one
you also store replaces it, one it has and you do not is inherited, and one it
has never had is added. Switching before any file is written is refused with
409 NO_INSTRUCTIONS.
Test it
Run a text turn through the agent with the files you stored. Nothing on this API
compiles what it stores, so this is where a [[partial]] your deployment does
not have shows up.
POST {deployment}{tenantPath}/chat/message
Authorization: Bearer <your deployment's chat credential>
Content-Type: application/json
{ "message": "Hi, are you open on Saturday?", "sessionId": null }{deployment} is wherever you host the deployment, since Subcore does not know
that address. The credential is the deployment's own, not the key you called
/agents with: either the CHAT_API_SECRET in its environment, or the key it
minted for itself, which the console shows on its page and which starts
sk_dep_. A deployment with neither answers without a credential.
sessionId: null asks for a new conversation, and the reply names it so you can
send the next turn into the same one. A turn that runs streams back as one JSON
object per line: a session line, then token lines, then finalize.
A turn refused before that stream opens answers with a status and a plain
{ "error": …, "code": … } instead, and this is where a file that does not
compile shows up. 401 is the wrong credential. 404 means no agent answers on
that routing key. 500 carries a code naming what failed, such as compile
for a file the deployment could not build, merge for one it refused, or
agent_missing for files stored under a directory it does not serve. 503
means it could not reach Subcore to read the configuration at all.
The body above is what the kit accepts unless your deployment defines its own shape, in which case send that one.
Point the number at it
Route the agent's phone number to {deployment}{tenantPath}/incoming-call.
What is not here
This site documents api.subcore.ai. Your deployment's own doors, such as
{tenantPath}/chat/message and {tenantPath}/incoming-call, belong to the
Subcore kit and are documented with it. The one above is set out here only
because testing an agent you just stored files for needs it.
The same goes for the deployment's own code: which resolver keys a
{{variable}} may name, and how to add one, are decisions made there rather
than here. What a stored file may contain is set out on
prompt files.