Skip to content

Approve (signed assertion required).

POST
/approvals/{approval_id}/approve
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 getIntegrationSelfapprover_keys).

  • Invalid/mismatched signature, unknown key_id, stale exp, or a decision mismatch → 403 approval-signature-invalid. The approval stays pending.
  • Already resolved or past expires_at409 approval-expired.
  • secrets optionally supplies requested secret-kind items (alias → value) — vaulted exactly like putConversationSecrets, 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.

approval_id
required
string
/^apr_[A-Za-z0-9]+$/

Internal approval ID.

Idempotency-Key
string
<= 255 characters

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.

Media type application/json

Body for approveApproval.

object
signature
required

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

Approver key ID (see getIntegrationSelfapprover_keys).

string
algorithm
required

Signature algorithm of the referenced key.

string
Allowed values: hmac-sha256 ed25519
exp
required

Assertion expiry (unix seconds). Must be in the future; recommended ≤ 5 minutes ahead. Stale assertions are rejected 403 approval-signature-invalid.

integer
value
required

The signature, base64url-encoded.

string
secrets

Optionally supplies requested secret-kind items — vaulted write-only, conversation-scoped, exactly like putConversationSecrets. The resumed run sees only the aliases.

object
key
additional properties
string
note

Free-text audit note recorded on the approval.

string
<= 1000 characters
Examples
Example approve_with_secret

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

Approval granted; the parked run resumes.

Media type application/json

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
object
required
string
Allowed value: approval
id
required
string
/^apr_[A-Za-z0-9]+$/
tenant_id
required
string
/^tnt_[A-Za-z0-9]+$/
conversation_id
required
string
/^con_[A-Za-z0-9]+$/
message_id
required

The assistant message parked on this approval.

string
/^msg_[A-Za-z0-9]+$/
status
required

expiredexpires_at passed unresolved; the parked run ended failed.

string
Allowed values: pending approved denied expired
reason
required

Agent-stated explanation of why approval is needed.

string
requested_items
required

What the agent needs (actions and/or secrets).

Array<object>

One thing the agent needs to proceed.

object
kind
required

action — permission to perform a described action; secret — a credential to be supplied under alias via the approve body’s secrets map.

string
Allowed values: action secret
description
required

Agent-stated need, human-readable.

string
alias

For secret items — the alias to vault the value under.

string
/^[A-Z][A-Z0-9_]{0,63}$/
expires_at
required

Resolution deadline; sticky sandboxes stay warm until then.

string format: date-time
resolved_by

Approver-key identity that resolved it (e.g. approver_key:apk_…); null while pending.

string | null
resolved_at

Resolution time; null while pending.

string | null format: date-time
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 approved

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.

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

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

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

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

Not found — the resource does not exist, was deprovisioned, or lies outside the integration key’s subtree (indistinguishable by design).

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 not_found

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

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

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

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