@agentpress/sdk
v0.15.0
Published
TypeScript SDK for the AgentPress API
Keywords
Readme
@agentpress/sdk
TypeScript SDK for the AgentPress webhook API.
- Package:
@agentpress/sdk(npm, public) - Output: Dual ESM (
.mjs) + CJS (.cjs) - Node: >= 22.0.0
- Dependencies:
josefor Partner MCP JWT verification; action webhook signing usesnode:crypto
Installation
npm install @agentpress/sdk
# or
bun add @agentpress/sdkQuick Start
import { AgentPress } from "@agentpress/sdk";
const client = new AgentPress({
webhookSecret: "whsec_your_secret_here",
});
// Send a webhook
await client.webhooks.send({
action: "my_action",
payload: { eventType: "test", data: { key: "value" } },
});Constructor Options
const client = new AgentPress({
webhookSecret: "whsec_...", // Required for default Svix sends, verify, and action management
baseUrl: "https://api.agent.press", // Default
timeout: 30_000, // Default, in milliseconds
org: "default-org", // Default org slug
apiKey: "sk_...", // Optional, reserved for future API auth
onRequest: (url, init) => {}, // Optional hook, called before every request
onResponse: (url, response) => {}, // Optional hook, receives a cloned Response
});All options are optional. webhookSecret is required when calling send with
the default Svix signing mode, HMAC/shared-token sends without an auth override,
verify, verifyOrThrow, constructEvent, or action management methods.
Validation rules:
webhookSecretmust start with"whsec_"or aConfigurationErroris throwntimeoutmust be a positive finite number- Trailing slashes on
baseUrlare stripped automatically
API Reference
The client exposes client.webhooks, client.actions, client.userApprovals, and client.partners.
client.webhooks.send(params)
Sends an arbitrary action webhook payload. By default the SDK uses Svix-style
signing with the client's webhookSecret, which matches the default
verification scheme for new AgentPress action webhooks. Pass auth to match
another verification scheme configured for the action webhook.
const response = await client.webhooks.send({
action: "my_webhook_action",
payload: {
eventType: "order.completed",
externalId: "order-456",
data: { total: 99.99 },
},
});eventType is the canonical discriminator and is recommended for new
integrations. Every payload must contain exactly one top-level, non-empty string
discriminator: eventType or the compatibility alias event. AgentPress
returns HTTP 400 when both keys, neither key, or an empty/non-string selected
value is sent. For event-only inputs, AgentPress uses the event value as the
canonical eventType for routing, persistence, and action creation while
retaining the original request payload unchanged as rawPayload provenance.
An existing event-based sender can use this compatibility envelope:
{
"event": "mir.created",
"timestamp": "2026-07-10T12:00:00.000Z",
"data": { "rating": 5 }
}Endpoint: POST {baseUrl}/webhooks/actions/{org}/{action}
action is the action webhook identifier shown on the AgentPress webhook detail
page. The older /webhooks/ingest/{org}/{identifier} listener endpoint is kept
only for compatibility APIs; new integrations should use send().
Params (WebhookSendParams):
| Field | Type | Description |
|-------|------|-------------|
| action | string | Webhook action identifier (used in URL path) |
| payload | Record<string, unknown> | Arbitrary JSON payload to sign and send |
| auth | WebhookSendAuth | Optional verification override. Defaults to Svix with the client's webhookSecret. |
Returns (WebhookResponse):
{
success: boolean;
actionId?: string; // ID of created or existing action
alreadyExists?: boolean; // Duplicate externalId
skipped?: boolean; // Compatibility field for skipped deliveries
buffered?: boolean; // HTTP 202: accepted but waiting on configuration
eventId?: string; // Buffered/skipped webhook_events row
reason?: string; // Machine-readable buffered/skipped reason
data?: Record<string, unknown>;
}Configuration/resolution gaps such as a missing action rule, disabled rule, or
unresolved user return HTTP 202 with buffered: true instead of throwing.
The delivery is persisted and appears in the Actions ledger for debugging and
retry.
Compatibility notes:
- Existing
client.webhooks.send()callers continue to work for action webhooks using the default Svix verification scheme. No SDK method was removed. - If the webhook is configured for
hmac_sha256,shared_token, ornone, pass the matchingauthoption. Manual sender scripts must send the matching headers. - Existing listener aliases and
/webhooks/ingest/{org}/{identifier}remain compatibility paths, but new integrations should usesend()and/webhooks/actions/{org}/{action}. 202withbuffered: trueis accepted delivery without a created action yet; checkresponse.bufferedbefore usingresponse.actionId.- For
verificationScheme: "none", the URL is the credential and can rotate when the webhook is created, switched tonone, or rotated. Copy the latest URL after those operations.
Throws: ConfigurationError (missing required secret/token), HttpError (non-2xx), TimeoutError
Signing details: Each request gets three headers automatically:
svix-id-- unique message ID (msg_<uuid>)svix-timestamp-- Unix secondssvix-signature--v1,<base64 HMAC-SHA256>
The signature input is ${msgId}.${timestamp}.${body}.
await client.webhooks.send({
action: "billing_events",
auth: {
scheme: "hmac_sha256",
secret: process.env.AGENTPRESS_WEBHOOK_SECRET!,
},
payload: {
eventType: "invoice.created",
data: { invoiceId: "inv_123" },
},
});Auth schemes:
| Scheme | Params | Notes |
|--------|--------|-------|
| svix | { secret?, msgId?, timestamp? } | Adds svix-id, svix-timestamp, and svix-signature. Uses webhookSecret when secret is omitted. |
| hmac_sha256 | { secret?, timestamp? } | Adds x-webhook-timestamp and x-webhook-signature using signHmacWebhookRequest. Uses webhookSecret when secret is omitted. |
| shared_token | { token? } | Adds x-webhook-token. Uses webhookSecret when token is omitted. |
| none | none | Sends no verification headers. Only use for capability-URL action webhooks intentionally configured with no header verification. |
For verificationScheme: "none", the action webhook URL itself is the
credential. Creating, switching, or rotating such a webhook may mint a new
unguessable webhookIdentifier; copy the latest URL from AgentPress before
updating manual sender scripts.
client.webhooks.verify(params)
Verifies an inbound Svix webhook signature. Returns true if valid, false if invalid or expired.
const isValid = client.webhooks.verify({
payload: rawRequestBody, // string or Buffer
headers: {
"svix-id": req.headers["svix-id"],
"svix-timestamp": req.headers["svix-timestamp"],
"svix-signature": req.headers["svix-signature"],
},
});
if (!isValid) {
return new Response("Unauthorized", { status: 401 });
}Params (WebhookVerifyParams):
| Field | Type | Description |
|-------|------|-------------|
| payload | string \| Buffer | Raw request body |
| headers["svix-id"] | string | Message ID header |
| headers["svix-timestamp"] | string | Unix timestamp header |
| headers["svix-signature"] | string | Signature header (may contain space-separated signatures) |
Verification rules:
- Timestamp must be within 5 minutes of current time (replay protection)
- Uses timing-safe comparison (
timingSafeEqual) - Supports multiple space-separated signatures in the header (any match = valid)
Throws: ConfigurationError if webhookSecret was not provided
client.webhooks.verifyOrThrow(params)
Same as verify but throws WebhookSignatureError on invalid signature. Useful in middleware.
try {
client.webhooks.verifyOrThrow({
payload: rawBody,
headers: {
"svix-id": headers["svix-id"],
"svix-timestamp": headers["svix-timestamp"],
"svix-signature": headers["svix-signature"],
},
});
// Signature is valid, proceed
} catch (error) {
if (error instanceof WebhookSignatureError) {
return new Response("Invalid signature", { status: 401 });
}
}client.webhooks.constructEvent(params)
Verify and parse an inbound webhook from AgentPress in one step. Combines signature verification with JSON parsing and returns a typed ActionCallbackPayload. This is the recommended way to handle incoming webhooks.
import { AgentPress, type ActionCallbackPayload } from "@agentpress/sdk";
const client = new AgentPress({ webhookSecret: "whsec_..." });
// Express example
app.post("/webhooks/agentpress", express.raw({ type: "application/json" }), (req, res) => {
const event = client.webhooks.constructEvent({
payload: req.body, // raw body string or Buffer
headers: {
"svix-id": req.headers["svix-id"] as string,
"svix-timestamp": req.headers["svix-timestamp"] as string,
"svix-signature": req.headers["svix-signature"] as string,
},
});
console.log(event.status); // "staged" | "approved" | "executing" | "completed" | ...
console.log(event.eventType); // "action.pending_approval" | "action.approved" | ...
console.log(event.agentResponse); // { text: "...", toolCalls: [...] }
res.status(200).json({ received: true });
});Params: Same as verify / verifyOrThrow (WebhookVerifyParams).
Returns (ActionCallbackPayload):
| Field | Type | Description |
|-------|------|-------------|
| actionId | string | Action identifier |
| status | ActionStatus | "pending", "staged", "approved", "executing", "completed", etc. |
| actionType | string | The action type (e.g., "order.created") |
| eventType | ActionEventType | Public callback event type |
| webhookAction | string \| null | Webhook action identifier used to create this action |
| stagedToolCall | StagedToolCall \| null | Tool awaiting approval for action.pending_approval callbacks |
| occurredAt | string | ISO 8601 timestamp for this callback event |
| completedAt | string | Backwards-compatible ISO 8601 timestamp field |
| resultSummary | string \| null | Human-readable execution summary when populated |
| sourceData | Record<string, unknown> | Original data from the inbound webhook |
| externalId | string \| null | External system identifier |
| userId | string \| null | AgentPress user ID |
| threadId | string \| null | Thread ID if a conversation was created |
| agentResponse | AgentResponse | Agent's text response and tool call results |
| errorMessage | string \| null | User-safe error message if the action failed |
| errorDetails | { code, provider, recoverable, userMessage } \| null | Structured metadata for branching/logging without raw provider errors |
| rejectionReason | string \| null | Reason if rejected by a human reviewer |
Throws: WebhookSignatureError (invalid/expired signature), ConfigurationError (missing secret), AgentPressError (invalid JSON)
client.actions.approve(actionId, params)
Approve a staged action, optionally modifying the tool call arguments before execution.
// Approve as-is
await client.actions.approve("act_123", {
action: "my_webhook_action",
});
// Approve with modified arguments
await client.actions.approve("act_123", {
action: "my_webhook_action",
editedToolCall: {
toolName: "sendEmail",
arguments: { subject: "Updated subject" },
},
});Endpoint: POST {baseUrl}/webhooks/actions/{org}/{action}/manage/{actionId}/approve
Params:
| Field | Type | Description |
|-------|------|-------------|
| actionId | string | Action ID (first positional argument) |
| params.action | string | Webhook action identifier (from callback's webhookAction field) |
| params.editedToolCall | object (optional) | Modified tool call to use instead of the original |
Returns (ActionManageResponse):
{
success: boolean;
actionId: string;
status: ActionStatus;
}Throws: ConfigurationError (missing secret), HttpError (non-2xx), TimeoutError
client.actions.reject(actionId, params)
Reject a staged action with an optional reason.
await client.actions.reject("act_456", {
action: "my_webhook_action",
reason: "Insufficient context to proceed",
});Endpoint: POST {baseUrl}/webhooks/actions/{org}/{action}/manage/{actionId}/reject
Params:
| Field | Type | Description |
|-------|------|-------------|
| actionId | string | Action ID (first positional argument) |
| params.action | string | Webhook action identifier (from callback's webhookAction field) |
| params.reason | string (optional) | Reason for rejection |
Returns: ActionManageResponse (same as approve)
Throws: ConfigurationError (missing secret), HttpError (non-2xx), TimeoutError
client.userApprovals.getWebhookMetadata(params)
Fetch webhook metadata and the ordered tool catalog used for per-user approval settings.
const metadata = await client.userApprovals.getWebhookMetadata({
webhookIdentifier: "partner_actions_webhook",
});
console.log(metadata.actionWebhookId);
console.log(metadata.availableTools.map((tool) => tool.toolName));Each availableTools item includes toolName, label, description, destructiveHint, and sensitiveHint. Use toolName from this response when rendering partner settings UIs; do not hardcode names such as review-reply tool slugs.
client.userApprovals.list(params)
Read approval rules for a webhook, optionally resolving an external auth user.
const { approvals } = await client.userApprovals.list({
webhookIdentifier: "partner_actions_webhook",
userId: "external_user_123",
authProvider: "partner_auth",
});If the external user has not authenticated into AgentPress yet, or the resolved user is not a member of the webhook's org, core returns HTTP 422 with code: "USER_NOT_PROVISIONED". Provision the user by sending them through the partner external-auth flow once, ensure they belong to the org, then retry.
client.userApprovals.create(params)
Create or upsert one stored approval rule. Current AgentPress core resolves actionWebhookId from webhookIdentifier, so partner apps do not need to store the webhook UUID.
const metadata = await client.userApprovals.getWebhookMetadata({
webhookIdentifier: "partner_actions_webhook",
});
const selectedTool = getSelectedToolFromYourUi(metadata.availableTools);
await client.userApprovals.create({
webhookIdentifier: metadata.webhookIdentifier,
userId: "external_user_123",
authProvider: "partner_auth",
toolName: selectedTool.toolName,
mode: "always_allow",
});client.userApprovals.sync(params)
Safely sync an Allow/Ask settings UI for one external user and one webhook. Use this for default approval settings.
const metadata = await client.userApprovals.getWebhookMetadata({
webhookIdentifier: "partner_actions_webhook",
});
const allowByToolName: Record<string, boolean> = getSettingsFromYourUi();
const result = await client.userApprovals.sync({
webhookIdentifier: metadata.webhookIdentifier,
userId: "external_user_123",
authProvider: "partner_auth",
rules: metadata.availableTools.map((tool) => ({
toolName: tool.toolName,
mode: allowByToolName[tool.toolName] ? "always_allow" : "ask",
})),
});
console.log(result.preservedDenyTools);sync is idempotent. mode: "always_allow" upserts an allow rule. mode: "ask" removes an existing allow rule for that listed tool. Existing always_deny rules are preserved and returned in preservedDenyTools; tools omitted from rules are untouched.
Error Handling
All errors extend AgentPressError. Import and use instanceof for specific handling.
import {
AgentPress,
AgentPressError,
ConfigurationError,
HttpError,
TimeoutError,
WebhookSignatureError,
} from "@agentpress/sdk";
try {
await client.webhooks.send({ action: "test", payload: {} });
} catch (error) {
if (error instanceof HttpError) {
// Non-2xx response from the server
console.log(error.statusCode); // e.g. 400, 404, 500
console.log(error.responseBody); // Raw response text
console.log(error.url); // Full request URL
} else if (error instanceof TimeoutError) {
// Request exceeded the configured timeout
} else if (error instanceof ConfigurationError) {
// Missing webhookSecret, invalid timeout, etc.
} else if (error instanceof WebhookSignatureError) {
// Only from verifyOrThrow -- invalid or expired signature
} else if (error instanceof AgentPressError) {
// Base class: fetch failures, unexpected non-JSON responses
}
}Error hierarchy:
AgentPressError (base)
ConfigurationError -- invalid options or missing webhookSecret / partnerMcp
HttpError -- non-2xx response (has statusCode, responseBody, url)
TimeoutError -- request exceeded timeout
WebhookSignatureError -- invalid/expired signature (from verifyOrThrow only)
PartnerTokenError -- Partner MCP JWT verification failure (has typed `reason`)
KeyRotationVerifyError -- Key-rotation webhook verification failure (has typed `reason`)Exported Values and Types
import { ACTION_EVENT_TYPES } from "@agentpress/sdk";
import type {
ActionCallbackPayload, // Inbound webhook payload from constructEvent()
ActionEventType, // "action.pending_approval" | "action.approved" | ...
ActionManageResponse, // Response from actions.approve() / actions.reject()
ActionStatus, // "pending" | "staged" | "approved" | "executing" | "rejected" | "completed" | "failed" | "expired"
AgentPressOptions, // Constructor options
AgentResponse, // Agent text response + tool calls
ApprovalMode, // "always_allow" | "always_deny"
ApprovalRuleSyncMode, // "always_allow" | "ask"
ApproveActionParams, // actions.approve() params
CreateUserApprovalParams, // userApprovals.create() params
DeleteUserApprovalParams, // userApprovals.delete() params
GetUserApprovalWebhookMetadataParams, // userApprovals.getWebhookMetadata() params
KeyRotationEvent, // Verified signing_key_rotation webhook payload
KeyRotationVerifyErrorReason, // Union of KeyRotationVerifyError.reason values
KeyRotationVerifyParams, // verifyKeyRotation() params
ListUserApprovalsParams, // userApprovals.list() params
ListUserApprovalsResponse, // userApprovals.list() response
PartnerMcpOptions, // AgentPressOptions.partnerMcp shape
PartnerTokenClaims, // Verified Partner MCP Spec v1 JWT claims
PartnerTokenErrorReason, // Union of PartnerTokenError.reason values
RejectActionParams, // actions.reject() params
StagedToolCall, // Tool call awaiting approval
StagedToolCallSummary, // Structured staged tool summary
SyncUserApprovalRule, // One tool rule for userApprovals.sync()
SyncUserApprovalsParams, // userApprovals.sync() params
SyncUserApprovalsResponse, // userApprovals.sync() response
ToolCallResult, // Individual tool call (name, arguments, result)
UpdateUserApprovalParams, // userApprovals.update() params
UserToolApproval, // Persisted user approval row
WebhookResponse, // Response from send
WebhookApprovalMetadata, // userApprovals.getWebhookMetadata() response
WebhookApprovalToolMetadata, // One ordered tool metadata row
WebhookSendParams, // send params
WebhookVerifyParams, // verify / verifyOrThrow / constructEvent params
} from "@agentpress/sdk";Full Example: Receiving Action Callbacks
import express from "express";
import { AgentPress, WebhookSignatureError, type ActionCallbackPayload } from "@agentpress/sdk";
const app = express();
const client = new AgentPress({ webhookSecret: process.env.WEBHOOK_SECRET });
// Recommended: use constructEvent() for verify + parse in one step
app.post("/webhooks/agentpress", express.raw({ type: "application/json" }), (req, res) => {
let event: ActionCallbackPayload;
try {
event = client.webhooks.constructEvent({
payload: req.body,
headers: {
"svix-id": req.headers["svix-id"] as string,
"svix-timestamp": req.headers["svix-timestamp"] as string,
"svix-signature": req.headers["svix-signature"] as string,
},
});
} catch (error) {
if (error instanceof WebhookSignatureError) {
return res.status(401).json({ error: "Invalid signature" });
}
throw error;
}
// event is fully typed as ActionCallbackPayload
console.log(`Action ${event.actionId} ${event.status}`);
if (event.agentResponse.text) {
console.log("Agent said:", event.agentResponse.text);
}
res.json({ received: true });
});Partner MCP Spec v1
Partners building an MCP server that federates with AgentPress can use the SDK's built-in verifiers for the two cryptographic primitives the spec requires: EdDSA JWT verification (against a remote JWKS) for inbound bearer tokens, and HMAC-SHA256 webhook verification for signing_key_rotation notifications.
This consolidates ~200 lines of hand-rolled JWT/JWKS/HMAC code that would otherwise live in every partner MCP integration. For the full wire protocol see the Partner MCP Spec v1 guide.
Configuration
Partner MCP helpers require a partnerMcp block on the constructor. All fields except clockTolerance and jwksCacheMaxAgeMs are required.
import { AgentPress } from "@agentpress/sdk";
// Per-org as of Partner MCP Spec v1 — each AgentPress org that registers you
// as a partner has its own signing keypair and JWKS URL. The org admin who
// registers you will share the org slug; your `issuer` + `jwksUrl` are
// derived from it.
const client = new AgentPress({
webhookSecret: "whsec_...", // Needed for verifyKeyRotation
partnerMcp: {
jwksUrl: "https://api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
issuer: "https://api.agent.press/orgs/<org-slug>",
audience: "https://mcp.example.com", // Your MCP's stable URL
expectedExtProvider: "partner_auth", // Required ext_provider claim value
clockTolerance: "30s", // Default: "30s" (accepts number of seconds or jose duration string)
jwksCacheMaxAgeMs: 3_600_000, // Default: 1 hour
},
});Running staging and production side-by-side
Staging and production MUST be separate AgentPress client instances, each with its own partnerMcp block. JWKS cache state is per-instance (not module-global), so two clients can run side-by-side in the same process without cross-talk.
const staging = new AgentPress({
webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET_STAGING,
partnerMcp: {
jwksUrl: "https://stg-api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
issuer: "https://stg-api.agent.press/orgs/<org-slug>",
audience: "https://mcp-staging.example.com",
expectedExtProvider: "partner_auth",
},
});
const production = new AgentPress({
webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET_PROD,
partnerMcp: {
jwksUrl: "https://api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
issuer: "https://api.agent.press/orgs/<org-slug>",
audience: "https://mcp.example.com",
expectedExtProvider: "partner_auth",
},
});Route requests to the correct client by inspecting the JWT's iss claim (or by a request-scoped environment flag upstream of the verifier). If the same partner integration serves multiple AgentPress orgs, spin up one client per org — do NOT share a single client across orgs (JWKS caches, iss, and aud are all org-scoped).
client.partners.verifyToken(token)
Verifies an EdDSA JWT issued by AgentPress against the configured remote JWKS. Returns typed PartnerTokenClaims on success; throws PartnerTokenError with a typed reason on failure.
Enforces the full spec: alg pinned to EdDSA, iss / aud exact-match, ext_provider exact-match, sub / jti / scope required and non-empty, kid header required, exp / iat validated with configurable skew (default ±30s).
import { AgentPress, PartnerTokenError } from "@agentpress/sdk";
import { Hono } from "hono";
const client = new AgentPress({ partnerMcp: { /* ... */ } });
const app = new Hono();
app.post("/mcp", async (c) => {
const auth = c.req.header("authorization") ?? "";
const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
if (!token) {
return c.text("Unauthorized", 401, {
"WWW-Authenticate": 'Bearer error="invalid_token", error_description="Missing bearer token"',
});
}
try {
const claims = await client.partners.verifyToken(token);
// claims is typed PartnerTokenClaims -- use `sub` to resolve the external user
// into your local user record (partner-domain logic):
const user = await resolveExternalUser(claims.ext_provider, claims.sub);
c.set("user", user);
c.set("claims", claims);
return handleMcpRequest(c);
} catch (err) {
if (err instanceof PartnerTokenError) {
const description = mapReasonToDescription(err.reason);
return c.text("Unauthorized", 401, {
"WWW-Authenticate": `Bearer error="invalid_token", error_description="${description}"`,
});
}
throw err;
}
});Error handling table — all PartnerTokenError reasons map to RFC 6750 error="invalid_token". Use error_description to distinguish:
| reason | HTTP | error | error_description |
|---|---|---|---|
| signature_invalid | 401 | invalid_token | Signature verification failed |
| issuer_mismatch | 401 | invalid_token | Issuer does not match |
| audience_mismatch | 401 | invalid_token | Audience does not match |
| expired | 401 | invalid_token | Token expired |
| not_yet_valid | 401 | invalid_token | Token not yet valid |
| missing_claim | 401 | invalid_token | Required claim missing or invalid |
| ext_provider_mismatch | 401 | invalid_token | ext_provider does not match |
| algorithm_not_allowed | 401 | invalid_token | Algorithm not allowed |
| kid_missing_or_unknown | 401 | invalid_token | Signing key id missing or unknown |
| malformed | 401 | invalid_token | Malformed token |
client.webhooks.verifyKeyRotation(params) + client.partners.refreshJwks()
Verifies a signing_key_rotation webhook signed by AgentPress with HMAC-SHA256 over the raw request body, then refreshes the JWKS cache so the next verified token picks up the new signing key without a failed-verify penalty.
The rotation webhook handler is optional — partners who don't implement it still pick up rotations automatically on the next kid miss (with one failed verify latency). Implement it to make rotations seamless.
import { AgentPress, KeyRotationVerifyError } from "@agentpress/sdk";
import { Hono } from "hono";
const client = new AgentPress({
webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET,
partnerMcp: { /* ... */ },
});
const app = new Hono();
app.post("/webhooks/agentpress/key-rotation", async (c) => {
// IMPORTANT: read the raw body, not c.req.json() -- HMAC is computed over
// the exact bytes AgentPress sent, not a re-serialized JSON object.
const rawBody = await c.req.text();
// Headers are passed verbatim as Record<string, string | undefined>; casing
// is handled internally.
const headers: Record<string, string | undefined> = {};
c.req.raw.headers.forEach((value, key) => {
headers[key] = value;
});
try {
const event = client.webhooks.verifyKeyRotation({
payload: rawBody,
headers,
});
// event is typed KeyRotationEvent
console.log(
`Rotation (${event.type}): ${event.retiredKid} -> ${event.newCurrentKid}`,
);
// Force-refetch the JWKS so the next verifyToken() picks up the new key.
await client.partners.refreshJwks();
return c.json({ received: true });
} catch (err) {
if (err instanceof KeyRotationVerifyError) {
const status = mapRotationReasonToStatus(err.reason);
return c.text(err.message, status);
}
throw err;
}
});Error handling table for KeyRotationVerifyError reasons:
| reason | HTTP | Notes |
|---|---|---|
| invalid_signature | 401 | HMAC mismatch |
| invalid_signature_format | 401 | x-agentpress-signature missing or not sha256=<hex> |
| timestamp_out_of_window | 401 | x-agentpress-timestamp outside ±5 min tolerance |
| invalid_timestamp | 401 | x-agentpress-timestamp missing or not a unix-seconds integer |
| malformed_payload | 400 | Body is not valid JSON or doesn't match the rotation event schema |
| payload_too_large | 413 | Body exceeds 8 KB cap |
For the full Partner MCP Spec v1 — token shape, JWKS discovery, rotation semantics, and error protocol — see the guide on docs.agentpress.dev.
File Structure
packages/sdk/src/
index.ts -- Public barrel export
client.ts -- AgentPress class, option validation + resolution
http.ts -- HttpClient (fetch wrapper with timeout, hooks, error parsing)
errors.ts -- Error class hierarchy
types.ts -- All exported types and interfaces
utils.ts -- randomMessageId() helper (msg_<uuid>)
actions/
client.ts -- ActionsClient (approve, reject)
partners/
client.ts -- PartnersClient (verifyToken, refreshJwks)
webhooks/
client.ts -- WebhooksClient (send, verify, verifyOrThrow, constructEvent, verifyKeyRotation)
signing.ts -- HMAC-SHA256 Svix-compatible sign + verify functions
keyRotation.ts -- Partner MCP Spec v1 rotation webhook HMAC + timestamp + payload validation