Documentation
DocsAPI reference

Agent Approval Protocol

What an adapter calls on behalf of a running agent. Create an instance and get its credential, ask for approval of a tool call, and wait for the answer.

Updated Oct 12, 2026

The Agent Approval Protocol (AAP) is the open protocol between an agent runtime and an approval provider. An adapter sits between the agent and its tools. When the agent calls a tool that needs approval, the adapter holds the call, describes it to the provider, and waits. The provider collects a decision from a person, or from the organization's approval pipeline, and hands it back. The adapter then runs the call or fails it.

The protocol carries decisions only. It never executes a tool or forwards its result. Approving or denying is the whole job, which keeps the surface to a handful of calls that any runtime can implement and any provider can serve. withHuman is one provider; an adapter written against this page works with any other.

A typical exchange:

  1. The adapter obtains a credential, either from withhuman agent install <runtime> on a developer machine or by creating an instance with a provisioner token.
  2. For a tool call, the adapter creates an approval request describing the tool and its arguments. The pipeline may decide on the spot; otherwise the request is pending.
  3. It retrieves the request with a wait, repeating until the status is approved, denied, expired, or cancelled, and acts on it.
  4. If the adapter stops waiting first (its runtime was interrupted or timed out), it cancels the request so reviewers stop seeing it.

Create an agent instance

POST/api/aap/v1/instancesBearer token

Creates a new instance of the agent the provisioner token belongs to and returns it together with its agent credential. Use this from a fleet or platform that starts agent replicas itself, so each replica gets a credential of its own. The bearer token must be a provisioner token (whp_); an agent credential is rejected. Requires an Idempotency-Key header scoped to the provisioner: repeating the same submission with the same key within seven days answers 200 with the original instance and credential, without creating another instance, renewing the credential or verifying the receiver again. Reusing a key with different input answers 409 with code idempotency_conflict, a key whose attempt is still running answers 409 with code idempotency_in_progress, and replaying a key whose instance was deleted answers 409 with code instance_deleted. Pass delivery to use the Agent Approval Protocol's asynchronous mode: before the instance is created, withHuman sends a signed webhook.verification message to the receiver, which must answer 200 with the same challenge within ten seconds, or the call fails with 422 and code webhook_verification_failed and no instance is created (the same key may be retried). A malformed or non-public receiver URL answers 400 with code invalid_webhook_url. Afterwards, every request of the instance that becomes approved, denied, expired or cancelled sends one signed request.resolved notification to the receiver, retried with backoff for up to seven days until it answers 2xx; notifications wait while the instance is disabled. The notification carries only IDs; read the decision with GET /api/aap/v1/requests/{id}. Both messages are signed with the Standard Webhooks scheme (webhook-id, webhook-timestamp and webhook-signature headers). Requires the deployment's secrets key; without it, delivery is refused with 501.

Request

Parameters

ParameterTypeDescription
Idempotency-Keyrequiredheader · string

A key of your choosing that identifies this call, so a retry does not act twice. See Idempotency in the API overview.

Request bodyoptional

A name for the instance, unique within the agent. Reviewers see it next to the agent's name.

Any JSON object to store with the instance, such as a region or pod name.

How long the credential stays valid, in seconds. A value above the provider's default is reduced to the default.

A webhook receiver for asynchronous mode. Install the registration ID and signing secret on the receiver before calling: the verification message names the registration so the receiver can pick the secret before it knows the instance ID.

Response

Response codes

StatusBodyDescription
200AAPProvisionedInstance

The instance and credential an earlier submission with the same idempotency key created.

201AAPProvisionedInstance

The new instance and its credential. Keep the credential's token; only a replay with the same provisioner token and idempotency key returns it again.

400ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

401ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

409ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

422ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

501ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

Response body200

An instance as the Agent Approval Protocol returns it to a provisioner. Credentials and the signing secret are never included.

credentialobjectrequired

Example

POST /api/aap/v1/instances
curl -X POST "$WITHHUMAN_URL/api/aap/v1/instances" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN" \
  -H "Idempotency-Key: 4f1c9a2e-7b3d-4e8f-a1c5-2d6b8e0f9a31" \
  -H "Content-Type: application/json" \
  -d '{
  "instance_name": "ci-runner-07",
  "instance_metadata": {
    "region": "eu-west"
  },
  "ttl_seconds": 86400,
  "delivery": {
    "type": "webhook",
    "url": "https://harness.example.com/aap/events",
    "registration_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "signing_secret": "string"
  }
}'
200 response
{
  "instance": {
    "id": "b29a43a7-1949-4b9f-9d61-b7ae66f85d31",
    "name": "ci-runner-07"
  },
  "credential": {
    "token": "whc_live_3f6b9c1d0e7a4b2c",
    "expires_at": "2026-09-18T12:00:00Z"
  }
}

Retrieve an agent instance

GET/api/aap/v1/instances/{id}Bearer token

Returns an instance of the provisioner token's agent with its receiver, if any. The signing secret and credentials are never returned. A deleted instance, or one of another agent, answers 404.

Request

Parameters

ParameterTypeDescription
idrequiredpath · uuid

Response

Response codes

StatusBodyDescription
200AAPInstance

The instance

401ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

404ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

Response body200

iduuidrequired
namestringrequired

Unique within the agent.

The verified receiver of an instance using asynchronous mode, returned when the instance is created. The signing secret is never returned.

Example

GET /api/aap/v1/instances/{id}
curl -X GET "$WITHHUMAN_URL/api/aap/v1/instances/{id}" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN"
200 response
{
  "id": "b29a43a7-1949-4b9f-9d61-b7ae66f85d31",
  "name": "ci-runner-07",
  "delivery": {
    "type": "webhook",
    "url": "https://harness.example.com/aap/events",
    "registration_id": "8246931c-4ce0-4c15-a94a-e9c078ad06e2"
  }
}

Update an agent instance

PATCH/api/aap/v1/instances/{id}Bearer token

Renames an instance of the provisioner token's agent, replaces its receiver, or removes it. A delivery object replaces the whole configuration, signing secret included, and is verified first exactly as at creation: the old configuration and name stay in place until verification succeeds, and a failure answers 422 with code webhook_verification_failed. Queued notifications then go to the new receiver with the new secret, with unchanged IDs and bodies. delivery: null removes the receiver and ends outstanding deliveries and retries. Requires an Idempotency-Key header scoped to the provisioner and the instance: repeating the same submission answers the original response without applying it again. A deleted instance answers 404.

Request

Parameters

ParameterTypeDescription
Idempotency-Keyrequiredheader · string

A key of your choosing that identifies this call, so a retry does not act twice. See Idempotency in the API overview.

idrequiredpath · uuid

Request body

Omit to keep the receiver, pass a complete configuration to replace it, or null to remove it.

Response

Response codes

StatusBodyDescription
200AAPInstance

The updated instance

400ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

401ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

404ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

409ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

422ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

501ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

Response body200

iduuidrequired
namestringrequired

Unique within the agent.

The verified receiver of an instance using asynchronous mode, returned when the instance is created. The signing secret is never returned.

Example

PATCH /api/aap/v1/instances/{id}
curl -X PATCH "$WITHHUMAN_URL/api/aap/v1/instances/{id}" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN" \
  -H "Idempotency-Key: 4f1c9a2e-7b3d-4e8f-a1c5-2d6b8e0f9a31" \
  -H "Content-Type: application/json" \
  -d '{
  "instance_name": "ci-runner-07",
  "delivery": {
    "type": "webhook",
    "url": "https://harness.example.com/aap/events",
    "registration_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "signing_secret": "string"
  }
}'
200 response
{
  "id": "b29a43a7-1949-4b9f-9d61-b7ae66f85d31",
  "name": "ci-runner-07",
  "delivery": {
    "type": "webhook",
    "url": "https://harness.example.com/aap/events",
    "registration_id": "8246931c-4ce0-4c15-a94a-e9c078ad06e2"
  }
}

Delete an agent instance

DELETE/api/aap/v1/instances/{id}Bearer token

Retires an instance of the provisioner token's agent: its credentials are revoked, its receiver is removed with every outstanding delivery, and each of its pending requests is cancelled without a notification. Requests that already reached an outcome are unchanged. The instance stays listed in the product, disabled, and cannot be enabled again. Deleting it again answers 204.

Request

Parameters

ParameterTypeDescription
idrequiredpath · uuid

Response

Response codes

StatusBodyDescription
204

The instance is deleted

401ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

404ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

Example

DELETE /api/aap/v1/instances/{id}
curl -X DELETE "$WITHHUMAN_URL/api/aap/v1/instances/{id}" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN"

Create an approval request

POST/api/aap/v1/requestsBearer tokenRequires request.create

Asks for approval to run a tool call. The organization's approval pipeline evaluates the request first and may approve or deny it on the spot, in which case the response already carries the decision. Otherwise the request goes to reviewers and comes back as pending; retrieve it with a wait to learn the outcome. A request nobody decides before its timeout becomes expired: the adapter does not run the call and tells the agent the approval timed out. An adapter that stops waiting should cancel the request so reviewers stop seeing it. Requires an Idempotency-Key header scoped to this operation and the authenticated instance. Reusing it with changed input returns 409.

Request

Parameters

ParameterTypeDescription
Idempotency-Keyrequiredheader · string

A key of your choosing that identifies this call, so a retry does not act twice. See Idempotency in the API overview.

Request body

toolstringrequired

The name under which the tool is defined: the name an MCP server advertises in tools/list, or the runtime's own name for a built-in tool such as Bash. Never the runtime's joined spelling such as mcp__stripe__issue_refund.

serverstring

The alias of the MCP server that defines the tool, as the runtime configured it. Present only for tools served over MCP. A label the adapter observed, not a verified identity.

argumentsobjectrequired

The exact arguments the tool will run with if approved. Reviewers see this as the description of the action, so it must be complete.

The agent's own explanation of why it wants to do this. Reviewers see it as a claim from the agent, separate from the arguments.

contextobject

Where the call comes from, as observed by the adapter rather than stated by the agent: for example the runtime, session id, or working directory.

timeoutstringrequired

How long the request may wait for a decision, as a duration such as 30m or 24h. Between one second and seven days. Once it passes, the request expires.

Response

Response codes

StatusBodyDescription
201AAPApprovalRequest

The request, either already decided by the pipeline or pending review

400ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

401ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

409

The idempotency key was already used for a different request

429

The organization has reached its limit of pending requests

Response body201

iduuidrequired
toolstringrequired
serverstring
argumentsobjectrequired
timeoutstringrequired
contextobject
statusenumrequired

One of pending, approved, denied, expired, cancelled

created_atdate-timerequired
deadline_atdate-timerequired

Immutable decision. An approved call must start before expires_at: 30 minutes after decided_at when the instance had webhook delivery at decision time, five minutes otherwise. Reads and retries never extend expires_at.

The creating instance used the approval. Present only once an approved request was consumed. An adapter that finds a consumption it did not make treats the approval as used.

Example

POST /api/aap/v1/requests
curl -X POST "$WITHHUMAN_URL/api/aap/v1/requests" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN" \
  -H "Idempotency-Key: 4f1c9a2e-7b3d-4e8f-a1c5-2d6b8e0f9a31" \
  -H "Content-Type: application/json" \
  -d '{
  "tool": "issue_refund",
  "server": "stripe",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "agent_reasoning": "Refunding the duplicate charge for [email protected].",
  "context": {
    "run_id": "4821",
    "framework": "claude-code"
  },
  "timeout": "30m"
}'
201 response: Pending review
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "pending",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z"
}
201 response: Approved, with the window to start the call
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "approved",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z",
  "decision": {
    "status": "approved",
    "note": "Refund the duplicate charge.",
    "decided_at": "2026-09-17T12:02:00Z",
    "expires_at": "2026-09-17T12:07:00Z"
  }
}

Retrieve an approval request

GET/api/aap/v1/requests/{id}Bearer tokenRequires request.read

Returns the request and, once it has been decided, its decision. Pass wait to hold the response until the request is decided or the wait runs out, whichever comes first. Only the instance that created the request may retrieve it. A request stops changing once it is approved, denied, expired, or cancelled.

Request

Parameters

ParameterTypeDescription
idrequiredpath · uuid

The request's id.

waitquery · string

How long to hold the response for a decision, as a duration such as 30s. Capped at 30 seconds. Omit to return immediately.

Response

Response codes

StatusBodyDescription
200AAPApprovalRequest

The request in its current state

401ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

404ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

Response body200

iduuidrequired
toolstringrequired
serverstring
argumentsobjectrequired
timeoutstringrequired
contextobject
statusenumrequired

One of pending, approved, denied, expired, cancelled

created_atdate-timerequired
deadline_atdate-timerequired

Immutable decision. An approved call must start before expires_at: 30 minutes after decided_at when the instance had webhook delivery at decision time, five minutes otherwise. Reads and retries never extend expires_at.

The creating instance used the approval. Present only once an approved request was consumed. An adapter that finds a consumption it did not make treats the approval as used.

Example

GET /api/aap/v1/requests/{id}
curl -X GET "$WITHHUMAN_URL/api/aap/v1/requests/{id}?wait=30s" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN"
200 response: Pending review
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "pending",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z"
}
200 response: Approved, with the window to start the call
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "approved",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z",
  "decision": {
    "status": "approved",
    "note": "Refund the duplicate charge.",
    "decided_at": "2026-09-17T12:02:00Z",
    "expires_at": "2026-09-17T12:07:00Z"
  }
}

Cancel an approval request

DELETE/api/aap/v1/requests/{id}Bearer tokenRequires request.create

Withdraws a pending request. Use it when the runtime that asked stops waiting for the answer: it was interrupted, timed out locally, or is shutting down. Reviewers stop seeing the request, notifications stop, and anyone holding it is released. Only the instance that created the request may cancel it; another instance of the same agent gets a 403. A cancelled request is final and answers cancelled with a protocol decision, so a long poll in flight returns at once. Cancelling again returns the same cancelled request. A request that was already approved, denied, or expired is unchanged and answers 409 with code already_terminal. This is an optional capability of the Agent Approval Protocol: adapters must treat a 404 or 405 from a provider that does not offer it as nothing to do.

Request

Parameters

ParameterTypeDescription
idrequiredpath · uuid

The request's id.

Response

Response codes

StatusBodyDescription
200AAPApprovalRequest

The request, now cancelled

401ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

403ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

404ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

409

The request was already decided or expired (code already_terminal)

Response body200

iduuidrequired
toolstringrequired
serverstring
argumentsobjectrequired
timeoutstringrequired
contextobject
statusenumrequired

One of pending, approved, denied, expired, cancelled

created_atdate-timerequired
deadline_atdate-timerequired

Immutable decision. An approved call must start before expires_at: 30 minutes after decided_at when the instance had webhook delivery at decision time, five minutes otherwise. Reads and retries never extend expires_at.

The creating instance used the approval. Present only once an approved request was consumed. An adapter that finds a consumption it did not make treats the approval as used.

Example

DELETE /api/aap/v1/requests/{id}
curl -X DELETE "$WITHHUMAN_URL/api/aap/v1/requests/{id}" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN"
200 response: Cancelled by the adapter
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "cancelled",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z",
  "decision": {
    "status": "cancelled",
    "note": "The agent stopped waiting.",
    "decided_at": "2026-09-17T12:02:00Z"
  }
}

Consume an approval

POST/api/aap/v1/requests/{id}/consumeBearer tokenRequires request.create

Marks an approved request as used, right before the adapter runs the call. Each approval runs one call: only one consume succeeds, so a second attempt to run the same call is refused. Call it after checking decision.expires_at, and do not start the call until it succeeds. Only the instance that created the request may consume it; another instance gets a 403. Requires an Idempotency-Key header that identifies this execution attempt: repeating the key returns the request as it is now, even after expires_at. The request takes no body; an empty body or {} is accepted. Answers 409 with code already_consumed when another key already consumed it, not_approved when the request is not approved, and approval_expired once expires_at has passed.

Request

Parameters

ParameterTypeDescription
idrequiredpath · uuid

The request's id.

Idempotency-Keyrequiredheader · string

A key of your choosing that identifies this call, so a retry does not act twice. See Idempotency in the API overview.

Request bodyoptional

object (application/json)

Response

Response codes

StatusBodyDescription
200AAPApprovalRequest

The request, now consumed

400ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

401ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

403ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

404ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

409

The approval was already used (code already_consumed), the request is not approved (not_approved), or the approval expired (approval_expired)

Response body200

iduuidrequired
toolstringrequired
serverstring
argumentsobjectrequired
timeoutstringrequired
contextobject
statusenumrequired

One of pending, approved, denied, expired, cancelled

created_atdate-timerequired
deadline_atdate-timerequired

Immutable decision. An approved call must start before expires_at: 30 minutes after decided_at when the instance had webhook delivery at decision time, five minutes otherwise. Reads and retries never extend expires_at.

The creating instance used the approval. Present only once an approved request was consumed. An adapter that finds a consumption it did not make treats the approval as used.

Example

POST /api/aap/v1/requests/{id}/consume
curl -X POST "$WITHHUMAN_URL/api/aap/v1/requests/{id}/consume" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN" \
  -H "Idempotency-Key: 4f1c9a2e-7b3d-4e8f-a1c5-2d6b8e0f9a31" \
  -H "Content-Type: application/json" \
  -d '{}'
200 response: Approved and consumed
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "approved",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z",
  "decision": {
    "status": "approved",
    "note": "Refund the duplicate charge.",
    "decided_at": "2026-09-17T12:02:00Z",
    "expires_at": "2026-09-17T12:07:00Z"
  },
  "consumption": {
    "consumed_at": "2026-09-17T12:02:04Z"
  }
}

Report an approved call's outcome

POST/api/aap/v1/requests/{id}/outcomeBearer tokenRequires request.create

Records whether the approved call succeeded, after the adapter consumed the approval and ran it. The outcome is for the record only: it never changes the request's status or decision. Report it once. Repeating the same body returns the same request; a different outcome answers 409 with code outcome_conflict. A request that was not consumed answers 409 with code not_consumed. Only the instance that created the request may report its outcome; another instance gets a 403.

Request

Parameters

ParameterTypeDescription
idrequiredpath · uuid

The request's id.

Request body

statusenumrequired

Whether the approved call succeeded.

One of succeeded, failed

notestring

A short note about the result, such as an error message or a reference.

Response

Response codes

StatusBodyDescription
200AAPApprovalRequest

The request with the outcome

400ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

401ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

403ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

404ErrorResponse

Error response. A 403 from a permission check carries ForbiddenDetails in error.details.

409

The approval was not consumed (code not_consumed), or a different outcome was already reported (outcome_conflict)

Response body200

iduuidrequired
toolstringrequired
serverstring
argumentsobjectrequired
timeoutstringrequired
contextobject
statusenumrequired

One of pending, approved, denied, expired, cancelled

created_atdate-timerequired
deadline_atdate-timerequired

Immutable decision. An approved call must start before expires_at: 30 minutes after decided_at when the instance had webhook delivery at decision time, five minutes otherwise. Reads and retries never extend expires_at.

The creating instance used the approval. Present only once an approved request was consumed. An adapter that finds a consumption it did not make treats the approval as used.

Example

POST /api/aap/v1/requests/{id}/outcome
curl -X POST "$WITHHUMAN_URL/api/aap/v1/requests/{id}/outcome" \
  -H "Authorization: Bearer $WITHHUMAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "succeeded",
  "note": "Refund issued"
}'
200 response: Consumed with its outcome
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "tool": "issue_refund",
  "arguments": {
    "amount": 4900,
    "reason": "duplicate_charge"
  },
  "timeout": "30m",
  "status": "approved",
  "created_at": "2026-09-17T12:00:00Z",
  "deadline_at": "2026-09-17T12:30:00Z",
  "decision": {
    "status": "approved",
    "note": "Refund the duplicate charge.",
    "decided_at": "2026-09-17T12:02:00Z",
    "expires_at": "2026-09-17T12:07:00Z"
  },
  "consumption": {
    "consumed_at": "2026-09-17T12:02:04Z",
    "outcome": {
      "status": "succeeded",
      "note": "Refund issued",
      "reported_at": "2026-09-17T12:02:06Z"
    }
  }
}