Skip to content

Upsert a tenant by external ID (the provisioning primitive).

PUT
/tenants/by-external-id/{external_id}
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.

external_id
required
string
>= 1 characters <= 255 characters

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).

Media type application/json

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
name

Display name. Omit to leave unchanged; null to clear.

string | null
<= 255 characters
default_repository_id

Set the tenant default repository (must be attached). Omit to leave unchanged; null to clear.

string | null
/^rep_[A-Za-z0-9]+$/
settings

Replaces the settings object wholesale when provided.

object
filler_enabled

Default filler-agent enablement — root of the filler cascade (tenant → conversation → message; most specific wins).

boolean
default: true
default_agent_type

Default runtime.agent_type for new conversations (open enum; e.g. claude-agent-sdk, codex, deepagent).

string
default: claude-agent-sdk
max_sticky_ttl_seconds

Cap on any conversation’s sticky lease TTL.

integer
default: 3600
max_concurrent_sticky

Cap on simultaneously leased sticky sandboxes.

integer
default: 5
metadata

Replaces the metadata map wholesale when provided.

object
<= 50 properties
key
additional properties
string
<= 500 characters
Examples

Cold path — first request from this host tenant

{
"name": "Acme Field Services",
"metadata": {
"host_plan": "premium"
}
}

Tenant already existed — provided fields merged (warm path).

Media type application/json

Unit of isolation mirroring one host-system tenant. Child of the integration key’s root tenant.

object
object
required
string
Allowed value: tenant
id
required
string
/^tnt_[A-Za-z0-9]+$/
external_id
required

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.

string | null
<= 255 characters
name
required

Display name. Nullable — external-ID-only creation is valid.

string | null
<= 255 characters
status
required

Suspended tenants reject conversation/message writes with 403 tenant-suspended; reads keep working.

string
Allowed values: active suspended
default_repository_id
required

Tenant default repository — the fall-through of the resolution cascade (message → conversation → user → role → tenant default). Must reference an attached repository.

string | null
/^rep_[A-Za-z0-9]+$/
settings
required

Tenant-level runtime defaults and caps. Downstream scopes (conversation, message) may only narrow within these.

object
filler_enabled

Default filler-agent enablement — root of the filler cascade (tenant → conversation → message; most specific wins).

boolean
default: true
default_agent_type

Default runtime.agent_type for new conversations (open enum; e.g. claude-agent-sdk, codex, deepagent).

string
default: claude-agent-sdk
max_sticky_ttl_seconds

Cap on any conversation’s sticky lease TTL.

integer
default: 3600
max_concurrent_sticky

Cap on simultaneously leased sticky sandboxes.

integer
default: 5
metadata
required

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
<= 50 properties
key
additional properties
string
<= 500 characters
created_at
required

RFC 3339 / ISO 8601 timestamp, UTC.

string format: date-time
updated_at
required

RFC 3339 / ISO 8601 timestamp, UTC.

string format: date-time
Examples
Example warm_existed

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).

Media type application/json

Unit of isolation mirroring one host-system tenant. Child of the integration key’s root tenant.

object
object
required
string
Allowed value: tenant
id
required
string
/^tnt_[A-Za-z0-9]+$/
external_id
required

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.

string | null
<= 255 characters
name
required

Display name. Nullable — external-ID-only creation is valid.

string | null
<= 255 characters
status
required

Suspended tenants reject conversation/message writes with 403 tenant-suspended; reads keep working.

string
Allowed values: active suspended
default_repository_id
required

Tenant default repository — the fall-through of the resolution cascade (message → conversation → user → role → tenant default). Must reference an attached repository.

string | null
/^rep_[A-Za-z0-9]+$/
settings
required

Tenant-level runtime defaults and caps. Downstream scopes (conversation, message) may only narrow within these.

object
filler_enabled

Default filler-agent enablement — root of the filler cascade (tenant → conversation → message; most specific wins).

boolean
default: true
default_agent_type

Default runtime.agent_type for new conversations (open enum; e.g. claude-agent-sdk, codex, deepagent).

string
default: claude-agent-sdk
max_sticky_ttl_seconds

Cap on any conversation’s sticky lease TTL.

integer
default: 3600
max_concurrent_sticky

Cap on simultaneously leased sticky sandboxes.

integer
default: 5
metadata
required

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
<= 50 properties
key
additional properties
string
<= 500 characters
created_at
required

RFC 3339 / ISO 8601 timestamp, UTC.

string format: date-time
updated_at
required

RFC 3339 / ISO 8601 timestamp, UTC.

string format: date-time
Examples
Example cold_created

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.

Media type application/problem+json

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
type
required

Problem type URI (registry slug).

string format: uri-reference
title
required

Short, human-readable summary of the problem type.

string
status
required

HTTP status code.

integer format: int32
detail

Human-readable explanation specific to this occurrence.

string
instance

URI reference identifying this occurrence.

string format: uri-reference
request_id

Correlation ID for support and log lookup.

string
conflicting_resource_id

On name-conflict, external-id-conflict, and resource-in-use: the ID of the existing/depended-on resource — fetch it and continue (replay recovery).

string
errors

On validation-error, field-level details.

Array<object>
object
pointer
required

JSON pointer to the offending field.

string
message
required

What failed.

string
Examples
Example unauthorized

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).

Media type application/problem+json

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
type
required

Problem type URI (registry slug).

string format: uri-reference
title
required

Short, human-readable summary of the problem type.

string
status
required

HTTP status code.

integer format: int32
detail

Human-readable explanation specific to this occurrence.

string
instance

URI reference identifying this occurrence.

string format: uri-reference
request_id

Correlation ID for support and log lookup.

string
conflicting_resource_id

On name-conflict, external-id-conflict, and resource-in-use: the ID of the existing/depended-on resource — fetch it and continue (replay recovery).

string
errors

On validation-error, field-level details.

Array<object>
object
pointer
required

JSON pointer to the offending field.

string
message
required

What failed.

string
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"
}