Manage organisations through the API¶
The concept behind organisations is explained in Organisations and customers.
On this page you administer your own organisation and the organisations below it with an API key: create an organisation for a customer, read and rename organisations, and create or revoke keys. You use it when you set customers up from your own system rather than by hand.
Your key needs the management capability (Switch it on). You do the same with a
mouse in Manage organisations in the console.
This capability does not grant access to a child organisation's files, prompts, skills or knowledge. Create a key there and use it to work with that organisation's data. Management permissions and data access are checked separately.
Switch it on¶
Your organisation has to permit management, and the key has to enable it. An administrator does
both in the console (Permitted capabilities,
Capabilities); a key that already holds management
registers another one with it in the body:
{ "name": "Platform automation", "capabilities": { "management": {} } }
The capability takes no configuration — an empty object is the whole of it. Check what your own key
holds with GET /key. Without management, every route on this page answers 404, the same answer
an unknown path gives (Troubleshoot a 404).
The routes come in pairs. A path with no organisation in it means your own — GET /organisation,
POST /keys, GET /admins — and a path naming one means an organisation below you:
GET /organisations/{id}, POST /organisations/{id}/keys. Your own id spelled the long way answers
the same as the short path.
Management permissions¶
Your key administers its own organisation and every organisation below it, and the two are not the same job. You run the organisations you were given to run; you may not promote, suspend, re-parent or delete the one you are.
| Operation | Your own organisation | An organisation below you |
|---|---|---|
read it (GET /organisation, GET /organisations/{id}) |
yes | yes |
name, timezone, data_retention_days, trace_retention_days, memory_retention_days, tracing_enabled, routing_config_id, assist_model, analysis_model, metadata |
yes; routing_config_id, assist_model and analysis_model unless key management is reserved |
yes |
inheritable_keys (which keys of metadata the organisations below read) |
yes | yes |
computer_internet (whether its computers may reach the internet) |
yes; 403 while an organisation above has it off |
yes; the same |
read its own document, only (GET /organisation/metadata, GET /organisation/metadata/effective) |
yes -- and needs no management at all |
— |
read its effective document (GET /organisations/{id}/metadata/effective) |
— | yes |
status, permitted_capabilities, admin_restrictions, monthly_cost_cap_eur, monthly_cost_cap_until (the end of this month, for a cap for this month only), monthly_cost_cap_counts (branch or own: whether the cap counts the organisations below), monthly_cost_cap_counts_erased (true or false: whether a cap that counts the branch keeps counting what a deleted organisation used this month) |
403 naming the field |
yes; a ceiling may not pass the ceiling of the organisation it sits directly under |
parent_id, audit_retention_days, legal_hold |
403 naming the field |
403; Artific's alone, wherever the organisation sits |
| create an organisation under it | yes | yes |
| delete it | 403 |
yes |
| list and read its keys | yes | yes |
| register, edit, rotate and delete its keys | yes, unless key management is reserved | yes |
| list its administrators | yes | yes |
| invite, re-invite and remove administrators | 403 |
yes |
read and write its policy (GET/PUT/DELETE /organisation/policy, GET/PUT/DELETE /organisations/{id}/policy) |
yes; writing unless key management is reserved | yes |
read its policy's published versions (GET /organisation/policy/versions, GET /organisation/policy/versions/{version}, and the same under /organisations/{id}/policy/versions) |
yes | yes |
try a policy document before writing it (POST /organisation/policy/decisions, POST /organisations/{id}/policy/decisions; Try a policy document) |
yes, unless key management is reserved | yes |
read and write its label vocabulary (GET/PUT/DELETE /organisation/label-vocabulary, GET/PUT/DELETE /organisations/{id}/label-vocabulary) |
reading with any key; writing unless key management is reserved | yes |
read and write its identity providers (GET/PUT /organisation/subject-issuers) |
yes; writing unless key management is reserved | — |
read, fill and empty its brand images (GET /organisation/assets, PUT/DELETE /organisation/assets/{slot}; Brand images) |
yes | — |
read and change its allowed hosts (GET/POST /organisation/allowed-hosts, DELETE /organisation/allowed-hosts/{host}; Allowed destinations) |
yes; changing returns 403 when key management is reserved |
— |
read and write its email domains and sign-in providers (/organisation/email-domains, /organisation/sign-in-providers) |
yes (Set up your own sign-in provider with a key) | — |
read its lifecycle events (GET /control-events, GET /organisations/{id}/control-events) |
yes | yes |
The identity-provider and allowed-hosts rows are about your own organisation rather than about one
below it: the providers whose token you may send
(Build for people) and the hosts your own calls reach. What
an organisation below you has there you read with a key you register in it. Its policy document
you read and write from above, at /organisations/{id}/policy: send a whole document, as for
your own, and start from GET /organisation/policy/default. The hosts an organisation below may
reach are the permit rules of kind egress_host in that document; the rules of the same kind in
your own document, and in the documents above you, limit it too
(Policy). Name the version you read in If-Match when you write, so
you do not overwrite a change someone saved after your read
(Hand out, rotate and withdraw keys).
Versions and drafts of the policy. Every PUT and DELETE of a policy document is published
at once as its next numbered version, and a key lists and reads the published ones at
/organisation/policy/versions. A draft is a person's, on the management twin only:
POST /tenants/{id}/policy/versions with document and "draft": true writes one,
POST /tenants/{id}/policy/versions/{version}/publish puts it in force and
DELETE /tenants/{id}/policy/versions/{version} discards it; the write and the publish name the
published document's version in If-Match. POST /tenants/{id}/policy/versions/{version}/decisions tries a version
with a key of the organisation and up to 50 questions, answered under that version and under the
published document; it decides nothing for real and writes nothing. No key reads a draft, and no
request is decided under one (Policy). A key tries a document it sends
inline instead (Try a policy document before you write it).
A refusal on one of those fields is a problem document naming it, for example Only an operator or a
parent organisation's administrator may change 'status'. Nothing is applied in part: a body that
carries one field you may not set changes none of them.
Anything outside your reach answers 404. An organisation that is not yours and not below you
is indistinguishable from one that does not exist, whichever route names it, so the tree cannot be
walked for ids.
A descendant's usage, its audit trail and its routing configurations are within reach of the same key: see Usage, audit and routing configurations. The deployment's price list is not: it is Artific's, and your organisation reads a price and never writes one.
Read and rename your own organisation¶
curl -s "$CORE_URL/organisation" -H "Authorization: Bearer $CORE_KEY"
The answer is the organisation as the console shows it: its id and name, where it sits
(parent_id), how it is kept apart (isolation) and which database it lives on
(database_ref), the capabilities it permits, its caps, its retention windows and what its parent
has reserved.
PATCH /organisation changes the fields of the table above that your own organisation may set:
curl -s -X PATCH "$CORE_URL/organisation" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "Northwind Trading", "data_retention_days": 90}'
data_retention_days is the window over your organisation's own content — threads, responses,
files, artifacts and memory records — and null keeps it for ever
(Data retention). It is your organisation's decision
about its own data, which is why your key may set it while it may not touch the window over the
audit trail. routing_config_id names one of your organisation's own fallback configurations, or
null for none (Fall back to another model).
tracing_enabled says whether Core records what it sends a model for your organisation, and
trace_retention_days gives traces a shorter window of their own than data_retention_days
(Traces). memory_retention_days gives memory records a window of
their own, longer or shorter. Under it a record is counted from its last write or confirmation, so a
fact you keep confirming stays, and an open commitment is never deleted; null leaves memory in
data_retention_days, counted from when each record was created. assist_model names the model
behind Describe what you need, or null for the deployment's default (Helper
model). The same model answers every turn of your own
helper that names no model. analysis_model names the model of improvement analyses,
under the same rules, and never falls back to assist_model (The analysis
model).
computer_internet says whether your organisation's computers may reach the internet
(Computers). A new organisation below
yours starts with it on when it counts for yours, and off otherwise; send "computer_internet":
false when you create it to start it off anyway. Your organisation decides it for itself after
that: your key may switch it on, and so may anyone above you. It counts only while every
organisation above yours has it on too, so switching yours off switches it off for every
organisation below you, and switching it on under an organisation that has it off is 403.
Switching it off does not end what is running: a computer keeps its open network until its
lifetime ends, the tunnels it has open through Core's proxy close at the proxy's next check (about
every 30 seconds), and DELETE /computers/{computer_id} ends it sooner. The answer carries
computer_internet_in_force, whether it counts right now, and computer_internet_available,
whether this deployment offers such computers at all; while that is false the switch is kept and
does nothing.
curl -s -X PATCH "$CORE_URL/organisation" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"computer_internet": true}'
Switch it on only when you accept what it means: code a model or a script runs there can send what it was given, files included, to any host on the internet.
timezone is an IANA zone name such as Europe/Amsterdam, in which your prompts' built-in date and
time values are read when a request or schedule names no zone of its own
(Timezone). A new organisation is set to UTC; send
"UTC" to go back to it, because null leaves the zone as it is. A zone Core does not know is
422 on timezone.
metadata is an opaque object of your own, at most 16 KiB serialised — Core stores it, returns it
and never reads it. It is where a platform built on Core keeps its own branding: colours,
copy, whatever it needs; its images go to Brand images instead. Leave the field out to
keep what it has, and send {} to clear it.
Every read of your organisation answers metadata_version beside it. Send it back in If-Match
on the PATCH and Core refuses the whole update with 409 (reason_code: "stale_version") when
someone changed metadata after your read, writing nothing; read again and repeat the change.
Without the header the PATCH replaces what is stored:
curl -s -X PATCH "$CORE_URL/organisation" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-H 'If-Match: "3f9a..."' \
-d '{"metadata": {"theme": {"primary": "#0a5"}}}'
Nothing of it flows down unless you say so. inheritable_keys lists the top-level keys of your
metadata that the organisations below yours may read as their own — at most 32, each named once,
replaced whole, [] for none. A key you do not list stays yours alone:
curl -s -X PATCH "$CORE_URL/organisation" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"metadata": {"theme": {"primary": "#0a5"}, "billing_note": "internal"},
"inheritable_keys": ["theme"]}'
GET /organisation/metadata/effective answers an organisation's effective document in one call:
its own keys, plus each key it lacks from the nearest organisation above it that lists that key and
holds it. An organisation above that lists a key but does not hold it is passed over, and the one
above that is asked.
{"metadata": {"logo_text": "Northwind", "theme": {"primary": "#0a5"}},
"inherited_from": {"theme": null}}
inherited_from names, for every key that came from above, the organisation it came from — or
null when your credential does not reach that organisation. A key of your own organisation never
reaches above it, so on this route every value is null; read
GET /organisations/{id}/metadata/effective with a key higher up to see the ids. A suspended
organisation above still lends its keys; a deleted one lends nothing, and neither does anything
above it.
Unlike everything else in this section, GET /organisation/metadata and
GET /organisation/metadata/effective need no management: any key
of your organisation reads them, and so does a subject token narrowed to one person, so a
browser-side frontend can read its own branding with whatever credential it was handed
(Build for people).
Create an organisation below you¶
An organisation's name is unique among the organisations under the same parent; creating, renaming or moving one into a name a sibling already has is refused 409. Duplicates that already exist stay until someone renames them.
POST /organisations names its parent, and the parent is any organisation you already reach —
your own, or one further down:
curl -s -X POST "$CORE_URL/organisations" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "Northwind Retail", "parent_id": "ten_4Kd9...",
"permitted_capabilities": ["threads", "knowledge", "management"]}'
| Field | What to send |
|---|---|
name |
what the organisation is called; the only required field |
parent_id |
the organisation it comes to sit under. Leave it out, or send null, and you are asking for an organisation that sits under nothing, which is Artific's to create (403) |
permitted_capabilities |
the ceiling for every key this organisation will have. It has to be inside the ceiling of the organisation you named as its parent; the first name outside it is refused with '<name>' is outside the capability ceiling of the organisation this one sits under. |
isolation |
shared unless you were told otherwise. schema gives the new organisation a database schema and role of its own, and is yours to choose whatever your own organisation uses |
database_ref |
do not send it. Naming a database is an operator's alone, and a key is never an operator; the new organisation defaults to the same physical database as the organisation you named as its parent. Sending the field is refused 403, even when it names that same database |
The answer is 201 with the new organisation and a Location header. Its status is
provisioning while Core prepares it; poll GET /organisations/{id} until it reads active. You
may register keys in it straight away, and they answer calls once it is active
(what the statuses mean). Until
then a call with one of its keys answers 409 problem+json with the title Conflict and the detail
The tenant is not ready., without a Retry-After header (the model-call routes carry the same
message in the vendor's error object): try again a little later.
You administer what you just created from the moment this call answers.
status is always the organisation's own. Suspending an organisation also stops every organisation
below it, and such an organisation keeps status: "active": read suspended_by_ancestor beside
it. true means an organisation above it is suspended, so its keys answer 403 and its runs,
schedules and knowledge work are stopped until that one is reactivated
(Suspend). The field does not say which organisation it is.
permitted_capabilities is likewise the organisation's own stored ceiling, the value you write. A
narrowing above does not change it: read capabilities_in_force beside it, the capabilities its
keys and administrators may use now, which leaves out whatever an organisation above no longer
permits, pseudonymisation and trust_envelope included. The field is read-only and does not say
which organisation withholds a capability.
DELETE /organisations/{id} erases an organisation below you and everything it owns, in the
background, with status on deleting until it is gone. While it is deleting, its keys answer the
same 409; once it is gone, its keys are gone with it and answer 401. An organisation that still has live
organisations under it is refused 409: erasure never cascades, so erase those first. Your own
organisation is 403 — an organisation does not erase itself.
Work inside an organisation below you¶
These routes administer organisations, never their content. To read a child's knowledge, store a prompt there or send a turn on its behalf, register a key in that child and call as that key:
curl -s -X POST "$CORE_URL/organisations/ten_9Fb2.../keys" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "Retail ingest", "capabilities": {"knowledge": {}}}'
The secret is in that response and nowhere else. A key of the child reaches the child's own data and
nothing above it: GET /key names the child, and asking that key about the organisation that
created it answers 404. Give it management as well and it administers the child and whatever
sits below that, which is how an application nests one level further down.
Hand out, rotate and withdraw keys¶
/keys is your own organisation's, and /organisations/{id}/keys is a child's. Both sets carry the
same seven operations: list, register, read, edit, rotate, create or replace the
callback secret
(POST …/keys/{key_id}/callback-secret) and delete.
curl -s -X POST "$CORE_URL/keys" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "Checkout service", "capabilities": {"threads": {}}, "monthly_cost_cap_eur": "250.00"}'
- The secret appears once.
secretis in the registration and the rotation response and in no other answer; Core keeps its hash and the twelve-charactersecret_prefix. Lose it and rotate. - What you may switch on is your organisation's ceiling,
managementincluded. A capability outside it answers409withcapability '<name>' is not permitted for this tenant, and so does one whose dependency is not enabled beside it:memoryneedsknowledge,runsneedsthreads,artifactsneedsfiles,evaluationsneedsagents. - Rotation ends the previous secret at once.
POST /keys/{key_id}/rotateanswers with the new one; there is no window in which both work. Rotating the calling key itself is allowed, and it ends the key that made the call: the response is the last thing the old secret earns. - You may bring the secret yourself. Registration and rotation take an optional
secretin the body ({"secret": "ack_..."}on the rotate route), which must have the exact shape Core issues:ack_and 43 URL-safe base64 characters, 32 random bytes from a secure generator. Another shape answers422onsecret, and so does a secret some key already has. The response carries it once, as it carries a generated one; leave the field out and Core generates it. Terraform uses this so a key's secret never enters its state. PATCH /keys/{key_id}narrows a key, its own included, which is how an application takes capabilities away from itself before handing a turn something risky. Its own monthly cap is the exception:monthly_cost_cap_eursent for the calling key itself answers403naming the field,nullincluded, because a key never sets its own ceiling. Another key holdingmanagement, or an administrator in the console, sets it.- A policy write can name the version it read. A policy document is replaced whole, so of two
writers that both read it and both write, the second removes the first's change and nobody
notices. Every read of a key answers
policy_version, and every read of an organisation's policy answersversion; an organisation without a document has aversiontoo. Send that value in the headerIf-Matchon the write (PATCHof a key,PUTorDELETEof an organisation's policy) and Core refuses it with409when the stored document changed in between. Nothing is written: read again and repeat your change. Without the header the write replaces whatever is stored.- The problem body of that refusal carries
reason_code: "stale_version". Switch on that field and not on the status: the same routes answer409for other reasons, and those bodies carry noreason_code. - A
PATCHof a key that sendsIf-Matchis refused whole when the key's document changed, also when its body carries nopolicy. If-Match: *matches whatever is stored.- Adding or removing an allowed host changes the organisation's
version: the list of hosts is part of the same document.
- The problem body of that refusal carries
expires_atends a key without deleting it. Set it on registration or on aPATCH, always in the future — a past instant answers422. Sendnullon aPATCHto lift an expiry the key already has; leaving the field out of aPATCHkeeps whatever it has. Past that instant the key is refused exactly as an unknown one is (401, the same body), whether presented as a secret or as a subject token minted from it. Rotation never resets it: rotation replaces the secret, not the lifetime.- A key never outlives the key that registers or edits it. Once your own key carries an
expires_at, every key it creates or edits — its own included — must carry one too, no later than its own: you may shorten your own key's expiry, or set one where it had none, but not extend or clear it, and the same holds for any key you hand out. Sending a wider value answers403, not422— the value is not malformed, an operator or an administrator may set the same one without being refused. A key with no expiry of its own puts no ceiling on anyone. DELETEstops further calls with that key, and ends every subject token minted from it at once (A token for the browser). What it created stays with the organisation.
The console shows and does the same things (Keys); a key registered from the API is an ordinary row there.
Usage, audit and routing configurations¶
Seven routes under /organisations/{id}/... answer for an organisation exactly what
/tenants/{id}/... answers for an administrator: GET .../usage, .../usage/summary,
.../usage/caps, .../usage/by-subject, .../usage/by-key, .../audit/events and
.../audit/events/export. Except for .../usage/by-key, they take the same parameters
(key_id, from, to, interval=day on the summary; limit on the by-subject read) and answer
the same shape as the routes in Usage and audit through the API. There is no
separate path for your own organisation: /organisations/{your own id}/usage works just as well,
since your key reaches its own organisation as one of its own roots
(Management permissions).
GET .../usage/by-key has no counterpart under /usage: it says what each key cost. It takes only
from and to; with neither named, the window is the current calendar month in UTC. items has
one entry per key with usage in the window, carrying key_id, rows, cost_eur and unpriced,
ordered by cost descending; a key that used nothing is not listed. keyless is the same entry for
what came in without a key, such as what somebody started from the console, with key_id null;
it is always present, at zero when there was nothing. The top-level cost_eur is the total of all
entries together. An unpriced above zero means the amount is a floor.
curl -s "$CORE_URL/organisations/ten_9Fb2.../usage/summary?interval=day&from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $CORE_KEY"
Routing configurations work on the same two paths: /routing-configs is your own organisation,
/organisations/{id}/routing-configs one below it — list, create, read, update and delete, and
list and read a configuration's published versions (.../{config_id}/versions), as
/tenants/{id}/routing-configs offers an administrator, whose create and update also take
draft: true and who alone publishes and discards a draft
(Fall back to another model):
curl -s -X POST "$CORE_URL/organisations/ten_9Fb2.../routing-configs" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "default", "chains": [{"primary": "gpt-5.6-sol", "fallbacks": ["claude-sonnet-5"]}]}'
Try a policy document before you write it¶
Before you PUT a policy document, ask how it would decide. Send the document you are about to
write, the id of a key of that organisation, and up to 50 questions: a kind, a name, an action
where the kind takes one, the labels the thing carries and, for a person, her attributes. The
document is a whole one, as for a PUT: a kind it writes no rule over is refused, so start from
GET /organisation/policy/default. The example below is cut short for that reason.
curl -s -X POST "$CORE_URL/organisation/policy/decisions" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"key_id": "key_01K...",
"document": {"rules": [
{"effect": "permit", "kind": "prompt", "match": {"names": ["private-*"]}},
{"effect": "permit", "kind": "egress_host", "match": {"names": ["api.jansen.nl"]}}]},
"questions": [{"kind": "prompt", "name": "private-welcome", "action": "use"},
{"kind": "egress_host", "name": "api.jansen.nl"}]}'
Each answer says, under draft, what the document you sent would decide, and under published
what the document in force decides now, with allowed, the reason, whose document decided
(organisation, key, ancestor or default) and the rule. A host is answered as an outbound
call would be, and nobody dials it. For an organisation below you, the same body goes to
POST /organisations/{id}/policy/decisions, with a key of that organisation as key_id.
The try refuses what the PUT would refuse, with the same answer: a label outside the
organisation's vocabulary is 422 naming the same field, so is a live check naming a judge model
the organisation cannot use, and a reservation of key management is the same 403. It writes nothing: no version, no event, no draft. A stored draft is a person's
and only the management console tries one.
Reserved key management¶
The organisation above yours can reserve key management to itself
(Reserved). It binds your key exactly as it binds your
administrators: registering, editing, rotating and deleting on your own organisation answer 403
with This organisation's parent has reserved key management to itself., and listing and reading
your keys are untouched. Setting routing_config_id is refused for the same reason, and so is any
write on /routing-configs of your own organisation — it is all key configuration.
Below you nothing is withheld, because there you are the organisation above.
Reserving usage figures or the audit trail binds your key the same way: GET
/organisations/{your own id}/usage and the other five answer 403 the moment your own
organisation is the one reserved, and a plain 200 for an organisation below it. GET /usage,
GET /audit/events and GET /key — your key asking for its own data without naming an
organisation — are never affected by a reservation.
Who administers an organisation¶
GET /admins lists the people who administer your own organisation, with their status. That is all
your own organisation offers: who administers it is decided from above it, so a key cannot invite
someone into the organisation that issued it.
Below you, the whole set is yours:
| Call | What it does |
|---|---|
GET /organisations/{id}/admins |
who administers that organisation. An empty page is a real answer |
POST /organisations/{id}/admins |
invite someone by email. created says whether an account was made, invite_sent whether the mail went out |
POST /organisations/{id}/admins/{user_id}/invite |
send the invitation again, and repair one that stopped half way |
DELETE /organisations/{id}/admins/{user_id} |
take that person's administration of that organisation away. Their account is untouched |
Someone listed with active: false accepted nothing yet, or was interrupted; re-sending the
invitation is the repair (Admins).
Management events¶
GET /control-events lists what happened to your own organisation: its own creation, its renames
and retention changes, its keys registered, updated, rotated and deleted, its administrators
invited and removed, and its exports asked for and downloaded. Each event carries
event, created_at, request_id and actor — your key's own id when a key did it, or
operator:… when a person did.
An event about an organisation below you belongs to that organisation's trail, not to yours: the
child you created finds its own creation on its trail. Read that trail from above with
GET /organisations/{id}/control-events, which takes the same limit, cursor and order and
answers the same shape; an organisation outside your reach is 404.
A parent that reserved your audit trail has not reserved this — an organisation's own lifecycle is the one record it always keeps. The same events are the console's Recent events and its management audit trail (Audit).
Brand images¶
Your organisation can keep up to 250 images in Core — a logo, a favicon, a picture for a sign-in
page, a light and a dark logo for each theme of an embedded assistant — and show them anywhere,
also to people who hold no key. Each image sits in a slot, a name you choose: a lowercase letter, then up to 31 lowercase letters, digits or hyphens (logo,
favicon, sign-in). Core never reads the name.
Put an image in a slot by sending the file itself as the body. Core accepts PNG, JPEG and WebP up
to 512 KiB, and decides the type from the file's own bytes, not from the Content-Type you send.
SVG is not accepted.
curl -s -X PUT "$CORE_URL/organisation/assets/logo" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: image/png" \
--data-binary @logo.png
{"object": "organisation_asset", "slot": "logo", "content_type": "image/png",
"size_bytes": 18342, "sha256": "9f2c...e41a", "url": "/api/v1/assets/9f2c...e41a",
"created_at": "2026-10-17T09:12:00Z", "updated_at": "2026-10-17T09:12:00Z"}
The answer is 200 whether the slot was empty or held an image, which the new one replaces.
GET /organisation/assets lists every slot with the same fields and without the bytes, and
DELETE /organisation/assets/{slot} empties one; it answers 204, also when the slot was already
empty.
| Answer | Meaning | What to do |
|---|---|---|
413 |
the image is over 512 KiB | make it smaller |
415 |
the bytes are no PNG, JPEG or WebP | convert the file; an SVG has to be rendered to PNG or WebP |
422 |
the body is empty, or the slot name does not fit the pattern | send the file, or use DELETE to empty a slot |
409 |
the organisation already fills 250 slots, or it is still being created or deleted | empty a slot first, or wait until the organisation is ready |
The public address¶
url is where anyone reads the image, with no key and no token. Put your Core host in front of
it, without /api/v1 from CORE_URL, which url already holds:
https://core.example.com/api/v1/assets/9f2c...e41a. You can use that address directly in an
<img> on a sign-in page, in a mail or in your own frontend, on any domain.
The address is named after the image's content, not after your organisation, so it says nothing
about who uploaded it. That also means it changes whenever the image does: after you replace the
logo, read the new url and use that. Browsers and other caches may keep an image for a year
(Cache-Control: public, max-age=31536000, immutable), so never reuse an old address for a new
picture; a new picture always comes with a new address. A reader that sends the ETag it got in
If-None-Match gets 304 and no bytes.
An address answers 404 once no organisation holds that image any more: after you empty the slot
or replace its image, unless another organisation happens to keep the same file.
Every image is your organisation's own: an organisation below yours inherits none of them and fills
its own slots with a key of its own (Work inside an organisation below you).
Filling and emptying a slot appear in your management events as
tenant.asset_set and tenant.asset_deleted. The archive you
take with you holds the images, and listing and emptying slots are tools on the
MCP endpoint (list_own_assets, delete_own_asset); filling one is not, because the image is
bytes and not a tool argument. A subject token gets 403 on these routes.
Deleting does not recall copies elsewhere. When you empty a slot, or your organisation is deleted, Core removes its own copy and the address stops answering. A browser, a mail service or a CDN that already fetched the image may keep showing its own copy for up to a year, and Core cannot reach those copies. Do not put anything in a brand image that you may need to withdraw quickly.
Take your data with you¶
An organisation that leaves takes an archive of everything Core holds for it. Ask for it, wait until it is ready, and download it:
curl -X POST "$CORE/api/v1/exports" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d '{}'
# 202, with Location: /api/v1/exports/exp_…
curl "$CORE/api/v1/exports/exp_…" -H "Authorization: Bearer $KEY"
# status: queued, running, then ready
curl -o archive.zip "$CORE/api/v1/exports/exp_…/download" -H "Authorization: Bearer $KEY"
The archive is a zip. manifest.json names the format and its version, the rows in each file and
what is deliberately not in it; organisation.json is your organisation's settings and policy
document; tables/ holds one JSON Lines file per kind of record — conversations with their
messages, responses, files and artifacts, knowledge, memory, prompts, skills, agents, tools,
evaluations, schedules, the audit trail and usage — and blobs/ the bytes of every file. Your brand images are in it too, their details under control/ and the images themselves under files/. Secrets
are never in it, encrypted or not: your connectors' and MCP servers' credentials are entered again
at your new provider.
Send {"subject": "anna"} and the archive holds only what was written for that one person: that
is how you answer an access or portability request (GDPR Articles 15 and 20). Below you, the same
works on /organisations/{id}/exports. A subject token gets 403 on every one of these calls:
the archive is asked for by the backend that holds the key.
The archive can be downloaded for export_retention_hours, 72 hours by default, as often as you
need; after that the download answers 409 and you ask for a new one. Every download is a
tenant.export_downloaded event, with who took it and how many traces the archive carried, in your organisation's management
events (GET /control-events). If the organisation above you reserved the audit trail
or usage, your archive leaves those rows out and says so in withheld.
A download that was cut off resumes where it stopped: send Range: bytes=<n>-, where <n> is the
number of bytes you already have (curl -C - -o archive.zip ... does this), and the answer is
206 with the rest. Each resumed request is a download of its own, with its own
tenant.export_downloaded event. A range that starts at or past the end of the archive answers
416 and is not recorded; curl -C - on a file that already downloaded completely gets that
answer.
Asking, reading and listing are tools on the MCP endpoint too (create_own_export,
get_own_export, list_own_exports); the download is not, because an archive is bytes and not
a tool result.
Administer over MCP¶
Every operation on this page is also a tool on the key endpoint, with no configuration
(Use Core from an MCP client). The tool is named after the
operation: get_own_organisation, update_own_organisation, create_organisation,
register_organisation_key, rotate_own_organisation_key, invite_organisation_admin,
list_organisation_control_events. A key without management is offered none of them.
Inside a turn running on Core they are the same tools under a core__ prefix —
core__create_organisation, core__register_organisation_key — so an agent can set an organisation
up in one turn (Give an agent the platform). A registration answers
with the new secret, and a tool result is part of the turn: the secret passes through the model and
is kept with what the turn stored. Register keys from your own application when that matters.
Keep management to few keys¶
A key holding management grants what an administrator grants: any capability inside your
organisation's ceiling, management among them. Such a key can therefore register a second key that
administers as much as it does. That second key is an ordinary row in the console and its
registration is in the trail, so nothing is hidden — but whoever holds the first secret decides it.
Treat that secret as you would an administrator's password.
In practice that means one key for administration rather than management added to a key that is
also serving turns, a monthly cap on it like any other key, and rotation the moment it has been
anywhere it should not have been. Every one of its actions is in the trail above, under its own key
id.