Skip to main content

Approvals & Webhooks

When a policy resolves to approval_required, OpenProdkt does not execute the request and returns a governed approval decision. SDK integrations may wait or poll for resolution; proxy mode never waits for approval and blocks the original request immediately.

SDK and proxy modes​

ModeWhile approval is pendingAfter approval
SDKThe SDK may wait for a bounded time, or return approval_id for app-managed polling or webhook handling.Your app retries with the same operation_id, approval_id, and payload_hash.
ProxyThe proxy immediately returns HTTP 403 or gRPC PERMISSION_DENIED; the protected upstream is not called. A CX/orchestrator polls or receives the approval webhook.The app retries the original request with the same stable operation_id; the client does not need to send approval_id.

Proxy mode is transparent to business logic. If the request contains a stable operation_id, OpenProdkt can automatically correlate the retry and consume the approval in a single successful redemption. This is an approval state transition only. approval_id is optional for the proxy client. Identical metadata alone cannot distinguish a retry from a separate operation.

A single redemption does not guarantee exactly-once execution by your upstream. If the authorization response is lost after redemption, OpenProdkt will not redeem that approval again, and the upstream outcome may be unknown. Make the business operation idempotent using the same operation_id and reconcile ambiguous outcomes in your application.

Proxy challenge​

When a proxied request needs approval, /v1/proxy/evaluate returns HTTP 403 immediately, with Retry-After: 30 and Cache-Control: no-store. The protected upstream is not called. gRPC callers receive PERMISSION_DENIED. The body contains:

FieldValue
codeapproval_required
retry_after_seconds30
operation_idEchoed when it matches ^[A-Za-z0-9._:-]{1,128}$; omitted otherwise
approval_expires_atWhen the pending approval expires
{
"code": "approval_required",
"retry_after_seconds": 30,
"operation_id": "refund-2026-001",
"approval_expires_at": "2026-09-13T11:15:00Z"
}

The challenge never includes the approval ID or the payload hash. The CX or orchestrator tracks the approval through polling or webhooks. The application retries the original request with the same operation_id after approval.

How it works​

The SDK flow does not silently replay your business request. Your application owns the retry unless it opts into a bounded SDK wait.

For details on plans and how licensing works, see Licensing & Distribution.

Choose a delivery model​

OptionHow it worksBest for
Async + Webhook (recommended)SDK apps or the CX/orchestrator receive an approvalId; OpenProdkt sends lifecycle events to a registered webhook endpointBackend jobs, agents
App-managed retry (SDK)Your app stores approvalId, a human approves in the Console, your app retries the original operation with the explicit tupleWeb/mobile apps
Short wait (SDK)The SDK blocks for a bounded time polling for resolution, then returns the result or a timeoutInteractive apps
Polling (SDK/CX)The SDK app or CX/orchestrator checks status via GET /v1/approvals/{id}Integrations without webhooks
Transparent proxyThe proxy immediately blocks pending requests; after approval, the app retries the original request with the same stable operation_idExisting applications behind the proxy

SDK integrations may wait or poll. Proxy integrations never wait for approval: they block immediately and rely on the CX/orchestrator to observe resolution before the request is retried.

Async + Webhook​

For SDK flows, your application receives the approval ID in the challenge. For proxy flows, the CX/orchestrator owns the approval handle; the protected application does not need it. Register a webhook endpoint from the Console (Settings → Webhooks) or via POST /v1/webhooks, with a URL and the event types you want: approval.pending, approval.approved, approval.rejected, approval.expired, approval.revoked. Each delivery includes a stable eventId in the body and X-OpenProdkt-Event-Id header, plus an X-OpenProdkt-Timestamp header. Verify X-OpenProdkt-Signature using the endpoint secret shown once at creation. During secret rotation, configure the receiver to accept both the previous and new secret until in-flight deliveries have drained. The versioned format is v1=sha256=<hex HMAC-SHA256> over the timestamp, an ASCII period, and the exact raw request body. Enforce a replay window on the timestamp and durably deduplicate on event ID before handling the event. The bundled SDKs accept timestamps within 5 minutes (Go) or 300 seconds (Python and TypeScript).

App-managed retry​

This explicit mode is for SDK integrations whose application manages the approval workflow. Store the operation_id, approvalId, payload_hash, and the exact asset identity and authorization context returned by the initial approval_required decision. When a reviewer resolves it in the Console (or your webhook fires), retry /v1/evaluate with the same operation_id, asset, transport, and metadata, plus the original approval_id and payload_hash:

{
"version": "v1",
"asset": { "id": "ast_e8f1a2c3" },
"operation_id": "refund-2026-001",
"approval_id": "apr_5c6d7e8f",
"payload_hash": "sha256:<hash returned by the initial decision>",
"transport": { "kind": "http" },
"metadata": { "operation": "refund", "amount": 9000 }
}

OpenProdkt recomputes the canonical payload hash and checks the organization, authenticated principal, operation, asset, transport, metadata, and policy revision binding. A changed context is rejected rather than treated as a new authorization. A pending approval remains pending; the first valid approved retry atomically redeems it and returns allow. Later attempts report the consumed status and do not authorize again. Rejected, expired, revoked, unknown, or mismatched approvals do not authorize the operation. The application must still make the business operation idempotent using the same operation_id to handle an ambiguous upstream result.

Transparent proxy retry​

Proxy mode is transparent to business logic. If the request contains a stable operation_id, OpenProdkt can automatically correlate the retry and consume the approval in a single successful redemption. This is an approval state transition only, not upstream execution. approval_id is optional for the proxy client. The CX/orchestrator polls the approval or receives its webhook; the protected application does not wait and does not need to know the approval ID. After approval, it resends the original request through the proxy with the same operation_id and authorization context. The proxy immediately blocks while approval is pending and never holds the original request open.

A single redemption is not exactly-once upstream execution. If the authorization response is lost after redemption, the approval cannot be redeemed again and the upstream outcome may be unknown. Make the business operation idempotent with the same operation_id and reconcile ambiguous outcomes. Without a stable operation_id, a matching request fingerprint alone is not enough to redeem an approval.

Short wait (SDK)​

SDKs expose an opt-in bounded wait — see SDK Manual — that polls internally and returns either the resolved decision or a timeout. This never replaces async delivery; it's for interactive UX where a user is actively waiting.

Polling (SDK/CX)​

GET /v1/approvals/{id} returns the current status of a single approval. SDKs expose get_approval_status() (Python), GetApprovalStatus() (Go), and getApprovalStatus() (TypeScript) for this scoped lookup; polling reads one approval and does not list or reevaluate requests. API keys must include the approvals:read scope. A decision-only key receives a 403 with code=insufficient_scope; the SDK reports this as a typed scope error rather than a PDP outage. Async delivery remains the recommended default.

An authorized reviewer may revoke a pending or approved approval with POST /v1/approvals/{id}/revoke. Revocation records revokedAt, clears any unconsumed grant, and emits approval.revoked; later explicit retries remain denied.

Approval scope​

An approval is never a permanent permission — it's scoped to a single request:

  • Asset — what the request was about
  • Policy — which rule triggered the review
  • Request/context — the metadata submitted with the decision
  • Requester — who or what made the call
  • Expiration — pending approvals expire 24 hours after creation and move to expired. An approved approval is redeemable within a 2-minute grant window (grant_expires_at); a redemption after that window is refused
  • Status — pending, approved, rejected, expired, revoked, or consumed after one successful redemption
{
"id": "apr_5c6d7e8f",
"decisionId": "dec_9f2a1c4e",
"assetId": "invoice-agent",
"policyId": "pol_8a1b2c3d",
"requesterId": "key_4f5e6d7c",
"status": "pending",
"expiresAt": "2026-09-13T11:15:00Z"
}

See API Reference for the full approvals and webhooks endpoint list, and SDK Manual for language-specific helpers.