Approve (signed assertion required).
const url = 'https://shiftagent.example.com/approvals/example/approve';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"signature":{"key_id":"apk_01hzx8host001","algorithm":"hmac-sha256","exp":1782813720,"value":"sig-base64url-example-aGVsbG8td29ybGQ"},"secrets":{"CRM_API_KEY":"example-value-vaulted-never-echoed"},"note":"Approved by supervisor on duty."}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://shiftagent.example.com/approvals/example/approve \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "signature": { "key_id": "apk_01hzx8host001", "algorithm": "hmac-sha256", "exp": 1782813720, "value": "sig-base64url-example-aGVsbG8td29ybGQ" }, "secrets": { "CRM_API_KEY": "example-value-vaulted-never-echoed" }, "note": "Approved by supervisor on duty." }'Resolves a pending approval positively and resumes the parked run.
Signature requirement (two-party control): the body carries a
signature assertion minted with a per-tenant approver key
registered out-of-band — never with the sk_int_ service key. The
adapter transports the assertion from the host’s approval authority
but cannot mint one; a rogue adapter (or agent) cannot self-approve.
Signing contract: compute the payload as the canonical JSON
{"approval_id":"<id>","decision":"approve","exp":<unix-seconds>}
(keys sorted, no whitespace), then signature.value = base64url(HMAC-SHA256(approver_key_secret, payload)) — or an Ed25519
signature over the same payload. signature.exp must be in the
future (recommended ≤ 5 minutes ahead); signature.key_id selects
the approver key (see getIntegrationSelf → approver_keys).
- Invalid/mismatched signature, unknown
key_id, staleexp, or a decision mismatch →403approval-signature-invalid. The approval stays pending. - Already resolved or past
expires_at→409approval-expired. secretsoptionally supplies requestedsecret-kind items (alias → value) — vaulted exactly likeputConversationSecrets, write-only, conversation-scoped. This is the approval⇄vault weave: the agent asks for an alias, the approver supplies it, the run resumes seeing only the alias.
On success the parked stream emits resumed and continues to
message_end.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”Internal approval ID.
Header Parameters
Section titled “Header Parameters ”Optional idempotency key (any unique string, e.g. a UUID; max 255 chars). Responses are cached 24h per (key principal, operation, key); replays return the original status and body with Idempotency-Replayed: true. Reusing a key with a different payload responds 409 idempotency-key-conflict.
Request Body required
Section titled “Request Body required ”Body for approveApproval.
object
Signed assertion authorizing an approval resolution. Signing payload
is the canonical JSON {"approval_id":"<id>","decision": "approve"|"deny","exp":<unix-seconds>} (keys sorted, no whitespace).
value = base64url of HMAC-SHA256(approver_key_secret, payload),
or an Ed25519 signature over the same payload. The approver key is
registered out-of-band and is cryptographically distinct from the
integration service key — the adapter transports assertions, it
cannot mint them.
object
Approver key ID (see getIntegrationSelf → approver_keys).
Signature algorithm of the referenced key.
Assertion expiry (unix seconds). Must be in the future; recommended ≤ 5 minutes ahead. Stale assertions are rejected 403 approval-signature-invalid.
The signature, base64url-encoded.
Optionally supplies requested secret-kind items — vaulted write-only, conversation-scoped, exactly like putConversationSecrets. The resumed run sees only the aliases.
object
Free-text audit note recorded on the approval.
Examples
Signed approve supplying the requested secret
{ "signature": { "key_id": "apk_01hzx8host001", "algorithm": "hmac-sha256", "exp": 1782813720, "value": "sig-base64url-example-aGVsbG8td29ybGQ" }, "secrets": { "CRM_API_KEY": "example-value-vaulted-never-echoed" }, "note": "Approved by supervisor on duty."}Responses
Section titled “ Responses ”Approval granted; the parked run resumes.
A human-in-the-loop gate raised by the agent mid-run. Resolution requires a signed assertion minted with a per-tenant approver key (never the sk_int_ service key).
object
The assistant message parked on this approval.
expired — expires_at passed unresolved; the parked run ended failed.
Agent-stated explanation of why approval is needed.
What the agent needs (actions and/or secrets).
One thing the agent needs to proceed.
object
action — permission to perform a described action; secret — a credential to be supplied under alias via the approve body’s secrets map.
Agent-stated need, human-readable.
For secret items — the alias to vault the value under.
Resolution deadline; sticky sandboxes stay warm until then.
Approver-key identity that resolved it (e.g. approver_key:apk_…); null while pending.
Resolution time; null while pending.
RFC 3339 / ISO 8601 timestamp, UTC.
RFC 3339 / ISO 8601 timestamp, UTC.
Examples
Resolved approval
{ "object": "approval", "id": "apr_01hzx8appr001", "tenant_id": "tnt_01hzx8acme001", "conversation_id": "con_01hzx8conv001", "message_id": "msg_01hzx8asst001", "status": "approved", "reason": "The CRM lookup requires a credential that is not on file for this conversation.", "requested_items": [ { "kind": "secret", "description": "API key for the CRM system", "alias": "CRM_API_KEY" } ], "expires_at": "2026-07-02T10:15:00Z", "resolved_by": "approver_key:apk_01hzx8host001", "resolved_at": "2026-07-02T10:02:10Z", "created_at": "2026-07-02T10:00:05Z", "updated_at": "2026-07-02T10:02:10Z"}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"}Forbidden — tenant-suspended (writes to a suspended tenant), insufficient-scope (key/token lacks the scope or a platform JWT reaches beyond its user), or approval-signature-invalid (approval assertion failed verification).
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
Suspended tenant rejects conversation writes
{ "type": "https://shiftagent.example.com/problems/tenant-suspended", "title": "Tenant suspended", "status": 403, "detail": "Tenant tnt_01hzx8acme001 is suspended; conversation writes are rejected.", "request_id": "req_01hzx8sus001"}Approval assertion failed verification
{ "type": "https://shiftagent.example.com/problems/approval-signature-invalid", "title": "Approval signature invalid", "status": 403, "detail": "Signature did not verify against approver key apk_01hzx8host001.", "request_id": "req_01hzx8sig001"}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"}