Skip to content

Overview

Host-system embedding surface for on-prem shiftagent — tenant/user provisioning by external ID, repository & skill registry, roles, conversations with streamed agent replies, and scoped write-only secret vaulting.

The shiftagent Integration API is the contract between a host system’s adapter and an on-prem shiftagent deployment. The host system fronts all end-user traffic; a stateless, client-specific adapter derives identity from the host’s own JWT and calls this API. shiftagent’s database holds all mapping state — the adapter never needs storage of its own.

Host system ── host JWT ──▶ Adapter (stateless) ── sk_int_… key ──▶ shiftagent Integration API

Conventions

  • JSON fields, query parameters, and enum values are snake_case; URL segments are kebab-case.
  • Every resource carries an object type discriminator and a prefixed ID (tnt_, usr_, rol_, rep_, skl_, crd_, con_, msg_).
  • Lists are cursor-paginated: { "object": "list", "data": [...], "has_more": true|false, "next_cursor": "..." } with limit, starting_after, and ending_before query parameters.
  • Errors are RFC 9457 application/problem+json (see the problem type registry below).
  • Timestamps are ISO 8601 / RFC 3339, always UTC.

Authentication

Two bearer credentials exist; every operation documents which it accepts.

SchemeTokenWho holds itUsed for
integrationKeysk_int_… service keyThe adapter (service principal)Everything: provisioning, registry management, tenant-wide reads, conversations
platformJwtShort-lived JWT from tokenExchangeA single end user’s request contextConversation and message operations scoped to that user

The integration key is role-mode (no user directory behind it) and is scoped to the integration’s root tenant — every tenant it provisions is a child of that root, and the key can never see outside its subtree. Use getIntegrationSelf (GET /integration/self) to introspect the key.

For per-user calls the adapter exchanges external IDs for a short-lived platform JWT via tokenExchange (POST /auth/token-exchange) and forwards requests under that token. There is no acting-as header: user context is always carried by the token, tenant-scope calls run under the service key directly.

getHealth (GET /health) is the only unauthenticated operation.

External ID namespacing

Tenants, users, and conversations are addressed by the host system’s identifiers via by-external-id subresources. External IDs are opaque to shiftagent: compared byte-exact after trimming, max 255 characters, case-sensitive. Adapters MUST namespace them at derivation so multiple host systems (or environments) never collide:

  • tenants — {ns}:tenant:{host_tenant_id} (e.g. acme:tenant:128231)
  • users — {ns}:user:{host_user_id} (e.g. acme:user:9f27c1)
  • conversations — {ns}:conversation:{host_conversation_id} (e.g. acme:conversation:ticket-4521) — derived deterministically from the host’s own conversation/thread identifier; this is what keys the continue-vs-create decision

The namespace {ns} is adapter configuration. The adapter owns canonicalization (e.g. lowercasing GUIDs) — shiftagent never normalizes.

Upsert semantics (PUT …/by-external-id/{external_id})

The provisioning primitive is an idempotent merge-upsert:

  • 201 — the resource did not exist and was created. An empty body {} is valid: external ID–only creation is guaranteed to succeed, all other fields are enrichable later.
  • 200 — the resource existed; provided fields were merged.
  • Per-field merge rules: provided → replaced; omitted → unchanged; explicit null → cleared (nullable fields only). Adapters that omit role_id on the warm path therefore never touch the role assignment.
  • Concurrent upserts of the same external ID are resolved by a uniqueness constraint: the winner gets 201, the loser gets 200 with the winner’s record. Neither errors — there is no 409 on this path.

409 conflicts with a conflicting_resource_id extension apply only to named sub-resources (repositories, roles, skills, credentials), where they make crashed provisioning runs recoverable: fetch the conflicting resource by ID and continue.

Idempotency

All POST operations accept an optional Idempotency-Key header. Responses are cached for 24 hours per (key principal, operation, idempotency key); a replay returns the original status and body with Idempotency-Replayed: true. The same key with a different request payload yields 409 idempotency-key-conflict. PUT upserts and DELETEs are idempotent by construction and do not need the header.

Streaming (NDJSON)

createMessage (and createConversation / upsertConversationByExternalId with initial_message) responds with application/x-ndjson: one JSON object per line. The stream is the platform’s native agent event stream — raw Claude Agent SDK events passed through verbatim — plus a small set of platform notice lines. Adapters translate this stream into whatever shape their host system needs; the platform does not re-encode it.

  • Agent events (the majority of lines): raw runtime events with a top-level type field — system (run init, carries the session id), stream_event (incremental deltas, including text_delta chunks), assistant / user (complete turn payloads incl. tool calls and tool results), and result (the terminal event, carrying the run outcome and usage). The event vocabulary is versioned by the X-Shiftagent-Stream-Format response header (claude-sdk/1); new event types are additive — ignore unknown lines.
  • Platform notices: {"object":"platform.event","type":"queued",...} capacity-hold lines (see below) and {"event":"ping"} keepalives.
  • Termination: every complete stream ends with exactly one terminal line — the agent result event (success or failure) or a {"object":"platform.event","type":"error"} line carrying an RFC 9457 problem object. A stream that closes without a terminal line MUST be treated as truncated; reconcile via listMessages (the run continues server-side and the assistant message lands in history regardless).
  • ?stream=false on createMessage blocks until the run completes and returns the persisted assistant Message as JSON instead.

Runtime, warm sandboxes & capacity

Each conversation runs on a sandboxed agent runtime, selected by runtime.agent_type (open enum — e.g. claude-agent-sdk, codex, deepagent; defaults from tenant settings). Two placement modes:

  • pooled (default) — each message claims a warm-pool sandbox and releases it when the run ends.
  • warm — the conversation keeps its sandbox alive between messages under a sliding idle timeout (idle_ttl_seconds, default 300): every message resets the timer; when it lapses the sandbox is recycled back to the pool. Warmth is a latency optimization, never state — after expiry the next message cold-starts a fresh sandbox and resumes the same session seamlessly. The most recent message’s runtime settings govern what happens next; an existing warm sandbox is always used when present (even if the latest request says pooled — it simply stops being kept warm afterward). Tenant settings cap the max idle TTL and max concurrent warm sandboxes.

When the pool is exhausted, the caller-chosen on_capacity strategy applies: reject (default) → 429 capacity-exhausted with Retry-After; hold → the request is held and the stream first emits queued platform notices (position, retry_hint_seconds) until a sandbox frees, bounded by a deployment-configured maximum hold time (then capacity-exhausted). getCapacity exposes live pool state for pre-checks.

Env vars & secrets (four scopes)

  • env — plaintext, non-secret run parameters. Never put secret material in env — values are visible to the agent runtime verbatim.
  • secrets — write-only alias → value maps, vaulted at the API boundary and never returned by any operation. The agent sees only aliases (e.g. {{secret:CRM_API_KEY}}); the egress proxy resolves aliases to real values at the network boundary on outbound calls. A rogue agent can exfiltrate nothing: it never holds real credentials.
  • Secrets exist at four scopes, resolved most-specific-first (message → conversation → user → tenant):
    • message — the secrets map on createMessage; ephemeral, lives for that run only.
    • conversationputConversationSecrets; destroyed when the conversation is archived or its tenant deprovisioned.
    • userputUserSecrets; persists across the user’s conversations.
    • tenant — the operator-managed credential registry (/credentials).
  • A run that references an alias vaulted at no scope fails fast: the stream terminates with a missing-secret problem naming the alias (?stream=false422). The host re-sends the message with the secret attached — a one-step retry. There is no mid-run pause.

Problem type registry

Errors use RFC 9457 application/problem+json. type URIs live under https://shiftagent.example.com/problems/{slug} (illustrative host — each deployment substitutes its own). Every problem carries request_id; conflict problems add conflicting_resource_id where applicable.

SlugStatusMeaning
validation-error422Request body or parameters failed validation (errors[] lists pointers)
not-found404Resource does not exist (or is outside the key’s subtree)
name-conflict409Named sub-resource already exists (conflicting_resource_id)
external-id-conflict409external_id already taken via plain create (conflicting_resource_id)
cross-tenant409Referenced resource belongs to a different tenant
conversation-archived409Write attempted on an archived conversation
resource-in-use409Guarded delete refused; dependents exist (conflicting_resource_id)
role-required422User has no role assigned; a conversation context cannot be resolved
tenant-suspended403Tenant is suspended; conversation writes rejected
insufficient-scope403Key or token lacks the required scope
idempotency-key-conflict409Same Idempotency-Key, different request payload
missing-secret422The run referenced a secret alias vaulted at no scope; re-send with the secret attached
capacity-exhausted429No sandbox available (or max hold time exceeded); retry after Retry-After
rate-limited429Too many requests; retry after Retry-After

The adapter’s service-principal integration key (Authorization: Bearer sk_int_…). Role-mode (no user directory behind it), scoped to the integration’s root tenant subtree. Grants the coarse scopes listed by getIntegrationSelf. Accepted by every authenticated operation.

Security scheme type: http

Bearer format: sk_int_… service key

Short-lived platform JWT minted by tokenExchange, carrying a single user’s identity. Accepted only by conversation and message operations, scoped to that user. Default TTL 15 minutes.

Security scheme type: http

Bearer format: JWT