Skip to main content

SDK Manual

Official clients for Go, Python, and TypeScript. All three expose the same enforcing authorize() contract and are released together at matching versions.

Current version: v0.5.5

LanguagePackageInstall
Gogithub.com/OpenProdkt/governance-sdk-gogo get github.com/OpenProdkt/governance-sdk-go@v0.5.5
Pythonopenprodkt-governance-sdkpip install openprodkt-governance-sdk
TypeScript@openprodkt/governance-sdknpm install @openprodkt/governance-sdk

Authentication​

Every SDK client takes a base URL and an API key. Create a key in the Console under API Keys, and grant it the decision:run scope to authorize operations. Applications that poll one approval also need approvals:read.

The secret is shown once at creation. Store it in your secret manager and never commit it or ship it to a browser.

Evaluate a decision​

The core call asks whether an action may proceed. It sends one /v1/evaluate request per operation attempt. Allow and lifecycle pass-through return normally; denial and other non-allow outcomes are typed errors. Every call requires a stable operation_id.

Go​

package main

import (
"context"
"errors"
"fmt"
"time"

"github.com/OpenProdkt/governance-sdk-go/governance"
)

func main() {
client := governance.NewClient("https://api.sandbox.openprodkt.com", "opk_live_xxx", 5*time.Second)

resp, err := client.Authorize(context.Background(), governance.AuthorizationRequest{
AssetKey: "billing.invoice-agent.production",
OperationID: "invoice-2026-001",
Metadata: map[string]interface{}{"amount": 9000, "environment": "production"},
})
if err != nil {
var pending *governance.PendingApprovalError
if errors.As(err, &pending) {
fmt.Println(pending.OperationID, pending.ApprovalID, pending.PayloadHash, pending.StatusURL, pending.ExpiresAt)
return
}
panic(err)
}

fmt.Println(resp.Decision, resp.DecisionID)
}

Python​

from openprodkt_governance import GovernanceClient, PendingApproval

client = GovernanceClient("https://api.sandbox.openprodkt.com", "opk_live_xxx")

try:
decision = client.authorize(
asset_key="billing.invoice-agent.production",
operation_id="invoice-2026-001",
metadata={"amount": 9000, "environment": "production"},
)
except PendingApproval as pending:
print(pending.operation_id, pending.approval_id, pending.payload_hash, pending.status_url, pending.expires_at)
else:
print(decision.decision, decision.decision_id)

TypeScript​

import { AuthorizationDeniedError, GovernanceClient, PendingApproval } from "@openprodkt/governance-sdk";

const client = new GovernanceClient("https://api.sandbox.openprodkt.com", "opk_live_xxx");

try {
const result = await client.authorize({
assetKey: "billing.invoice-agent.production",
operationId: "invoice-2026-001",
metadata: { amount: 9000, environment: "production" },
});
console.log(result.decision, result.decisionId);
} catch (error) {
if (!(error instanceof PendingApproval)) throw error;
console.log(error.operationId, error.approvalId, error.payloadHash, error.statusUrl, error.expiresAt);
}

Handle all three outcomes​

authorize() returns normally only for allow and lifecycle pass-through. HTTP 202 throws PendingApproval, which carries the stable operation ID, approval ID, canonical payload hash, asset reference, status URL, and expiry. Denial, rejection, expiry, revocation, consumed redemption, identity collision, operation conflict, quota, invalid input/configuration, and PDP failure have distinct typed outcomes.

try {
await client.authorize({ assetId: "ast_invoice", operationId: "invoice-2026-001" });
await executePayment();
} catch (error) {
if (error instanceof PendingApproval) await parkForReview(error.operationId, error.approvalId);
else if (error instanceof AuthorizationDeniedError) await rejectWithReason(error.response.reason);
else throw error;
}

Persist the stable operation_id with your business operation. Policy-evaluated outcomes may also include a decisionId. Unknown-asset pass-throughs create no decision record or ID. A registered asset with no policies returns a decisionId but is not billed.

Handling approval_required​

See Approvals & Delivery for the full picture: async webhook delivery is the recommended default, with app-managed retry, a short-wait option, and polling available for other integration shapes.

Async + webhook (recommended). Register a webhook endpoint once; your app resumes the original operation when the approval.approved event arrives, independent of any SDK call.

App-managed retry. Store the pending error's operationId, approvalId, payloadHash, asset identity, and normalized context. After approval, retry the same authorize() call with the tuple. The PDP returns normally only after atomically consuming the approval.

Short wait. For interactive UX, block briefly for resolution instead of returning immediately:

const result = await client.authorizeAndWait(
{ assetId: "ast_invoice", operationId: "invoice-2026-001", metadata: { amount: 9000 } },
{ timeoutMs: 10_000, initialIntervalMs: 250, maxIntervalMs: 1_000 },
);
result = client.authorize(asset_id="ast_invoice", operation_id="invoice-2026-001", metadata={"amount": 9000}, wait_for_approval_seconds=10)
resp, err := client.AuthorizeAndWait(ctx, governance.AuthorizationRequest{
AssetID: "ast_invoice", OperationID: "invoice-2026-001",
Metadata: map[string]interface{}{"amount": 9000},
}, 10*time.Second, 1*time.Second)

Short wait never replaces async delivery for production workloads — it exists for cases where a user is actively waiting on the result.

Limits and defaults. Wait values are bounded in every SDK:

SDKSynchronous waitStatus wait limitPolling defaults
Pythonwait_for_approval_seconds 0 to 30timeout_seconds 0 to 300Initial 1.0 s, maximum 5.0 s, 0.1 jitter
GoAuthorizeAndWait up to 30 sWait options up to 300 sInitial 1 s, maximum 5 s
TypeScripttimeoutMs up to 30,000Status timeoutMs up to 300,000Initial 250 ms, default timeout 10 s

Status polling reads one approval and creates no usage.

Polling. getApprovalStatus(approvalId) / get_approval_status(approval_id) / GetApprovalStatus(ctx, id) reads one scoped approval. API keys require approvals:read; a decision-only key receives a typed scope error rather than a PDP outage.

Method reference​

The same operations are available in all three SDKs. Naming follows each language's convention — authorize / Authorize, listAssets / list_assets / ListAssets.

AreaOperations
Decisionsauthorize, playgroundCheck
AssetslistAssets, createAsset, updateAsset, deleteAsset
DiscoveryingestAssetDiscovery, listAssetDiscoveries, deleteAssetDiscovery
GroupslistGroups, createGroup, updateGroup, deleteGroup
PolicieslistPolicies, createPolicy, deletePolicy
ApprovalslistApprovals, getApprovalStatus, waitForApproval, approveApproval, rejectApproval
API keyslistApiKeys, createApiKey, rotateApiKey, revokeApiKey
HistorylistDecisionExplorer, listAuditEvents, usage
AccountrequestCode, verifyCode, register, me, settings

Each call requires the matching API key scope. See Auth and Access for the full scope list.

Reporting detected assets​

If you run a detection point that observes AI tools in your infrastructure, use the discovery ingest call. It requires the asset_discovery:write scope.

client.ingest_asset_discovery({
"discovery_id": "dsc_01J...",
"asset": {
"key": "billing.refund-api",
"displayName": "Billing Refund API",
"kind": "rest_api",
"environment": "production",
},
"metadata": {"operation": "refund", "amount": 250},
})

Discovery is observation only. It does not evaluate a policy and does not return a decision. Observations appear in the Console for a human to review and promote.

Protocol adapters​

HTTP and gRPC adapters normalize their own transport context into the same /v1/evaluate contract. gRPC streams are authorized once, at stream establishment; messages on an established stream are not authorized individually. A stream retried after an approval challenge must carry the stored approval tuple (approval_id, payload_hash, and the same operation_id). MCP is a future first-class adapter (MCP -> MCP PEP -> PDP), not a generic HTTP wrapper; it will explicitly normalize operations such as tools/call, tool/server identity, session context, and tool arguments.

Framework setup​

Install the optional extras when you use a framework integration:

  • Python FastAPI: pip install "openprodkt-governance-sdk[fastapi]". Use create_authorization_dependency to authorize routes and install_fastapi_exception_handlers(app) to map non-allow outcomes to HTTP responses: denial to 403, pending approval to 202, quota to 402, and PDP failure to 503.
  • Python gRPC: pip install "openprodkt-governance-sdk[grpc]". The client and server interceptors authorize each call or stream once, before the handler runs.
  • Go net/http: wrap your handler with Client.HTTPMiddleware, which authorizes each request and passes non-allow outcomes to your handler callback. Read the allow decision inside the handler with DecisionFromHTTPRequest.
  • TypeScript: authorizeFetchRequest for Fetch-based runtimes and authorizeNodeRequest for Node. For gRPC, use createGrpcClientInterceptor or createGrpcServerInterceptor.

Framework adapters take the operation ID from an explicit argument or the Idempotency-Key header. They never read identity from metadata.actor.

Versioning​

All three SDKs are released together with matching version numbers, so v0.5.5 behaves identically across languages. Pin an exact version in production and upgrade deliberately.

Next steps​