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
| Language | Package | Install |
|---|---|---|
| Go | github.com/OpenProdkt/governance-sdk-go | go get github.com/OpenProdkt/governance-sdk-go@v0.5.5 |
| Python | openprodkt-governance-sdk | pip install openprodkt-governance-sdk |
| TypeScript | @openprodkt/governance-sdk | npm 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:
| SDK | Synchronous wait | Status wait limit | Polling defaults |
|---|---|---|---|
| Python | wait_for_approval_seconds 0 to 30 | timeout_seconds 0 to 300 | Initial 1.0 s, maximum 5.0 s, 0.1 jitter |
| Go | AuthorizeAndWait up to 30 s | Wait options up to 300 s | Initial 1 s, maximum 5 s |
| TypeScript | timeoutMs up to 30,000 | Status timeoutMs up to 300,000 | Initial 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.
| Area | Operations |
|---|---|
| Decisions | authorize, playgroundCheck |
| Assets | listAssets, createAsset, updateAsset, deleteAsset |
| Discovery | ingestAssetDiscovery, listAssetDiscoveries, deleteAssetDiscovery |
| Groups | listGroups, createGroup, updateGroup, deleteGroup |
| Policies | listPolicies, createPolicy, deletePolicy |
| Approvals | listApprovals, getApprovalStatus, waitForApproval, approveApproval, rejectApproval |
| API keys | listApiKeys, createApiKey, rotateApiKey, revokeApiKey |
| History | listDecisionExplorer, listAuditEvents, usage |
| Account | requestCode, 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]". Usecreate_authorization_dependencyto authorize routes andinstall_fastapi_exception_handlers(app)to map non-allow outcomes to HTTP responses: denial to403, pending approval to202, quota to402, and PDP failure to503. - 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 withClient.HTTPMiddleware, which authorizes each request and passes non-allow outcomes to your handler callback. Read the allow decision inside the handler withDecisionFromHTTPRequest. - TypeScript:
authorizeFetchRequestfor Fetch-based runtimes andauthorizeNodeRequestfor Node. For gRPC, usecreateGrpcClientInterceptororcreateGrpcServerInterceptor.
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
- Developer Guide for errors, pagination, and retries
- API Reference for the raw HTTP surface
- Swagger UI to try calls in the browser