Upsert a tenant by external ID (the provisioning primitive).
const url = 'https://shiftagent.example.com/tenants/by-external-id/example';const options = { method: 'PUT', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"name":"Acme Field Services","metadata":{"host_plan":"premium"}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request PUT \ --url https://shiftagent.example.com/tenants/by-external-id/example \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "name": "Acme Field Services", "metadata": { "host_plan": "premium" } }'Idempotent get-or-create-or-refresh keyed on the host system’s tenant ID. This is the cold/warm provisioning primitive — adapters call it on every request and branch on the status code:
201— created (cold path): continue with repository attachment, role creation, and user upserts.200— existed (warm path): provided fields were merged; skip the cold-path steps.
An empty body {} is valid — a tenant can be created with nothing but
its external ID and enriched later. Merge semantics per field:
provided → replaced; omitted → unchanged; null → cleared.
Races are safe: concurrent upserts of the same external_id collapse
on a uniqueness constraint — the winner receives 201, the loser
200 with the winner’s record. No 409 is possible on this path.
Suspended tenants are returned as-is (status: "suspended"); the
upsert never reactivates a tenant implicitly — use updateTenant.
No Idempotency-Key is needed: PUT retries are inherently safe.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”The host system’s identifier, namespaced by the adapter (e.g. {ns}:tenant:{id} / {ns}:user:{id}). Opaque to shiftagent — compared byte-exact after trimming; case-sensitive; URL-encode reserved characters (: → %3A where required by the client).
Request Body required
Section titled “Request Body required ”Merge-upsert body for upsertTenantByExternalId. Every field is optional — {} performs external-ID-only get-or-create. Per field: provided → replaced, omitted → unchanged, null → cleared.
object
Display name. Omit to leave unchanged; null to clear.
Set the tenant default repository (must be attached). Omit to leave unchanged; null to clear.
Replaces the settings object wholesale when provided.
object
Default filler-agent enablement — root of the filler cascade (tenant → conversation → message; most specific wins).
Default runtime.agent_type for new conversations (open enum; e.g. claude-agent-sdk, codex, deepagent).
Cap on any conversation’s sticky lease TTL.
Cap on simultaneously leased sticky sandboxes.
Replaces the metadata map wholesale when provided.
object
Examples
Cold path — first request from this host tenant
{ "name": "Acme Field Services", "metadata": { "host_plan": "premium" }}External-ID-only creation (empty body)
{}Warm path — refresh drift from the host JWT
{ "name": "Acme Field Services"}Responses
Section titled “ Responses ”Tenant already existed — provided fields merged (warm path).
Unit of isolation mirroring one host-system tenant. Child of the integration key’s root tenant.
object
Host system’s tenant ID (namespaced by the adapter). Unique across the integration; natural key for upsertTenantByExternalId. Null only for tenants created via plain createTenant without one.
Display name. Nullable — external-ID-only creation is valid.
Suspended tenants reject conversation/message writes with 403 tenant-suspended; reads keep working.
Tenant default repository — the fall-through of the resolution cascade (message → conversation → user → role → tenant default). Must reference an attached repository.
Tenant-level runtime defaults and caps. Downstream scopes (conversation, message) may only narrow within these.
object
Default filler-agent enablement — root of the filler cascade (tenant → conversation → message; most specific wins).
Default runtime.agent_type for new conversations (open enum; e.g. claude-agent-sdk, codex, deepagent).
Cap on any conversation’s sticky lease TTL.
Cap on simultaneously leased sticky sandboxes.
Free-form string key–value map for host/adapter bookkeeping (e.g. a host-side reference ID). Max 50 keys; values max 500 chars. Replaced wholesale when provided in updates.
object
RFC 3339 / ISO 8601 timestamp, UTC.
RFC 3339 / ISO 8601 timestamp, UTC.
Examples
Warm path — tenant existed, name refreshed
{ "object": "tenant", "id": "tnt_01hzx8acme001", "external_id": "acme:tenant:128231", "name": "Acme Field Services", "status": "active", "default_repository_id": "rep_01hzx8fieldops", "settings": { "filler_enabled": true, "default_agent_type": "claude-agent-sdk", "max_sticky_ttl_seconds": 3600, "max_concurrent_sticky": 5 }, "metadata": { "host_plan": "premium" }, "created_at": "2026-07-02T09:30:00Z", "updated_at": "2026-07-02T10:15:00Z"}Tenant created (cold path).
Unit of isolation mirroring one host-system tenant. Child of the integration key’s root tenant.
object
Host system’s tenant ID (namespaced by the adapter). Unique across the integration; natural key for upsertTenantByExternalId. Null only for tenants created via plain createTenant without one.
Display name. Nullable — external-ID-only creation is valid.
Suspended tenants reject conversation/message writes with 403 tenant-suspended; reads keep working.
Tenant default repository — the fall-through of the resolution cascade (message → conversation → user → role → tenant default). Must reference an attached repository.
Tenant-level runtime defaults and caps. Downstream scopes (conversation, message) may only narrow within these.
object
Default filler-agent enablement — root of the filler cascade (tenant → conversation → message; most specific wins).
Default runtime.agent_type for new conversations (open enum; e.g. claude-agent-sdk, codex, deepagent).
Cap on any conversation’s sticky lease TTL.
Cap on simultaneously leased sticky sandboxes.
Free-form string key–value map for host/adapter bookkeeping (e.g. a host-side reference ID). Max 50 keys; values max 500 chars. Replaced wholesale when provided in updates.
object
RFC 3339 / ISO 8601 timestamp, UTC.
RFC 3339 / ISO 8601 timestamp, UTC.
Examples
Cold path — new tenant, no repository attached yet
{ "object": "tenant", "id": "tnt_01hzx8acme001", "external_id": "acme:tenant:128231", "name": "Acme Field Services", "status": "active", "default_repository_id": null, "settings": { "filler_enabled": true, "default_agent_type": "claude-agent-sdk", "max_sticky_ttl_seconds": 3600, "max_concurrent_sticky": 5 }, "metadata": { "host_plan": "premium" }, "created_at": "2026-07-02T09:30:00Z", "updated_at": "2026-07-02T09:30:00Z"}Missing or invalid credentials — no bearer token, an unknown/revoked sk_int_ key, or an expired platform JWT.
RFC 9457 problem+json error envelope. type is a URI under https://shiftagent.example.com/problems/{slug} (deployment host substituted); see the API-level problem registry for every slug.
object
Problem type URI (registry slug).
Short, human-readable summary of the problem type.
HTTP status code.
Human-readable explanation specific to this occurrence.
URI reference identifying this occurrence.
Correlation ID for support and log lookup.
On name-conflict, external-id-conflict, and resource-in-use: the ID of the existing/depended-on resource — fetch it and continue (replay recovery).
On validation-error, field-level details.
object
JSON pointer to the offending field.
What failed.
Examples
Missing or invalid bearer token
{ "type": "https://shiftagent.example.com/problems/insufficient-scope", "title": "Unauthorized", "status": 401, "detail": "Provide a valid sk_int_ service key or platform JWT.", "request_id": "req_01hzx8auth001"}Unprocessable — validation-error (schema/semantic validation failed; errors[] lists JSON-pointer details) or role-required (user has multiple roles and no role_id was given).
RFC 9457 problem+json error envelope. type is a URI under https://shiftagent.example.com/problems/{slug} (deployment host substituted); see the API-level problem registry for every slug.
object
Problem type URI (registry slug).
Short, human-readable summary of the problem type.
HTTP status code.
Human-readable explanation specific to this occurrence.
URI reference identifying this occurrence.
Correlation ID for support and log lookup.
On name-conflict, external-id-conflict, and resource-in-use: the ID of the existing/depended-on resource — fetch it and continue (replay recovery).
On validation-error, field-level details.
object
JSON pointer to the offending field.
What failed.
Examples
Field-level validation failure
{ "type": "https://shiftagent.example.com/problems/validation-error", "title": "Validation error", "status": 422, "detail": "One or more fields failed validation.", "errors": [ { "pointer": "/skill_access/skill_ids/0", "message": "skl_01hzx8unknown does not belong to the effective repository." } ], "request_id": "req_01hzx8val001"}Ambiguous role at conversation creation
{ "type": "https://shiftagent.example.com/problems/role-required", "title": "Role required", "status": 422, "detail": "User usr_01hzx8jane001 holds 2 roles; pass role_id explicitly.", "request_id": "req_01hzx8role01"}