Upsert a user by external ID.
const url = 'https://shiftagent.example.com/tenants/example/users/by-external-id/example';const options = { method: 'PUT', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"email":"jane.doe@acme.example.com","display_name":"Jane Doe","role_ids":["rol_01hzx8csr001"]}'};
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/example/users/by-external-id/example \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "email": "jane.doe@acme.example.com", "display_name": "Jane Doe", "role_ids": [ "rol_01hzx8csr001" ] }'Idempotent get-or-create-or-refresh keyed on the host system’s user ID
within the tenant. 201 created / 200 existed. An empty body {}
is valid — external-ID-only creation always succeeds; enrich later.
Merge semantics per field: provided → replaced; omitted →
unchanged; null → cleared. In particular, omitting role_ids on
the warm path leaves role assignments untouched; providing it replaces
the full set.
On creation the user is automatically attached to an S3-style
storage bucket (storage.provider: "platform"); link a
host-owned bucket later via updateUser.
Deactivated ≠ absent: upserting an existing suspended user returns
200 with status: "suspended" and does not reactivate — the
adapter must never resurrect users implicitly (reactivate explicitly
via updateUser). Token exchange for suspended users fails 403.
Races collapse on the per-tenant uniqueness of external_id: winner
201, loser 200. If a racing caller references roles the winner has
not created yet, role_ids may be [] and assigned later via
assignUserRole.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”Internal tenant ID.
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 upsertUserByExternalId. Every field optional — {} performs external-ID-only get-or-create. Per field: provided → replaced, omitted → unchanged, null → cleared. Omitting role_ids leaves role assignments untouched (the warm-path contract); providing it replaces the full set.
object
Profile field; null clears.
Profile field; null clears.
Replaces the full role set when provided (409 cross-tenant if any role belongs to another tenant). [] is valid — assign later via assignUserRole.
Per-user repository override; null clears.
Replaces the metadata map wholesale when provided.
object
Examples
Cold-path step 4 — create the user with its role
{ "email": "jane.doe@acme.example.com", "display_name": "Jane Doe", "role_ids": [ "rol_01hzx8csr001" ]}Warm path — profile refresh, role_ids untouched
{ "email": "jane.doe@acme.example.com", "display_name": "Jane Doe"}External-ID-only creation (empty body)
{}Responses
Section titled “ Responses ”User already existed — provided fields merged.
A host-system end user within one tenant. Effective skills = union over assigned roles of each role’s resolved skills.
object
Host system’s user ID (namespaced by the adapter). Unique per tenant; natural key for upsertUserByExternalId.
Optional profile field — enrichable after creation.
Optional profile field — enrichable after creation.
suspended (deactivated) users fail token exchange and cannot start conversations; they are never auto re-provisioned by upsert.
Assigned roles (also manageable via assignUserRole).
Per-user repository override in the resolution cascade.
S3-style storage bucket attached to a user or conversation. Platform-assigned automatically at creation; host-owned buckets can be linked via update (provider: "external").
object
platform — bucket provisioned and owned by the deployment; external — host-linked BYO bucket.
S3-style URI of the bucket root (e.g. s3://bucket/prefix).
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.
Example
{ "object": "user", "status": "active", "storage": { "provider": "platform" }}User created; storage bucket auto-attached.
A host-system end user within one tenant. Effective skills = union over assigned roles of each role’s resolved skills.
object
Host system’s user ID (namespaced by the adapter). Unique per tenant; natural key for upsertUserByExternalId.
Optional profile field — enrichable after creation.
Optional profile field — enrichable after creation.
suspended (deactivated) users fail token exchange and cannot start conversations; they are never auto re-provisioned by upsert.
Assigned roles (also manageable via assignUserRole).
Per-user repository override in the resolution cascade.
S3-style storage bucket attached to a user or conversation. Platform-assigned automatically at creation; host-owned buckets can be linked via update (provider: "external").
object
platform — bucket provisioned and owned by the deployment; external — host-linked BYO bucket.
S3-style URI of the bucket root (e.g. s3://bucket/prefix).
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
New user with auto-attached platform bucket
{ "object": "user", "id": "usr_01hzx8jane001", "tenant_id": "tnt_01hzx8acme001", "external_id": "acme:user:9f27c1", "email": "jane.doe@acme.example.com", "display_name": "Jane Doe", "status": "active", "role_ids": [ "rol_01hzx8csr001" ], "default_repository_id": null, "storage": { "provider": "platform", "bucket_uri": "s3://shiftagent-tenant-acme/usr_01hzx8jane001" }, "metadata": {}, "created_at": "2026-07-02T09:36:00Z", "updated_at": "2026-07-02T09:36: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"}Not found — the resource does not exist, was deprovisioned, or lies outside the integration key’s subtree (indistinguishable by design).
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
Unknown resource
{ "type": "https://shiftagent.example.com/problems/not-found", "title": "Not found", "status": 404, "detail": "No tenant with external_id acme:tenant:999999.", "request_id": "req_01hzx8nf001"}Conflict — name-conflict / external-id-conflict (unique name or external ID taken; conflicting_resource_id names the holder — fetch it and continue), resource-in-use (guarded delete refused), cross-tenant (referenced resource belongs to another tenant), conversation-archived (write to an archived conversation), approval-expired (approval already resolved or expired), or idempotency-key-conflict (same key, different payload).
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
Named sub-resource already exists — recoverable
{ "type": "https://shiftagent.example.com/problems/name-conflict", "title": "Name conflict", "status": 409, "detail": "A role named \"csr\" already exists in this tenant.", "conflicting_resource_id": "rol_01hzx8csr001", "request_id": "req_01hzx8conf01"}Guarded delete refused
{ "type": "https://shiftagent.example.com/problems/resource-in-use", "title": "Resource in use", "status": 409, "detail": "Repository is attached to 1 tenant and pinned by 2 roles.", "conflicting_resource_id": "tnt_01hzx8acme001", "request_id": "req_01hzx8used01"}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"}