Documentation Index
Fetch the complete documentation index at: https://turnkey-0e7c1f5b-taylor-eng-4112-webhooks-v2-docs.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Webhooks V2 sends HTTPS POST requests to endpoints that you register with Turnkey. Each endpoint can subscribe to one or more event types, and every delivery includes Turnkey headers. Signed deliveries also include Ed25519 signature headers so your receiver can verify that the request came from Turnkey before processing it.
Webhooks V2 is the recommended integration path for new webhook integrations. Legacy activity webhooks configured with FEATURE_NAME_WEBHOOK are still described at the end of this page for existing integrations.
Create an endpoint
Create webhook endpoints from a server-side client using an API key, or from any Turnkey client that can submit signed activities for your organization. The endpoint URL must be HTTPS and must resolve to a public destination.
The SDK call below wraps the /public/v1/submit/create_webhook_endpoint activity. See Manage endpoints for the full set of webhook endpoint APIs, including update and delete.
import { Turnkey } from "@turnkey/sdk-server";
const turnkey = new Turnkey({
apiBaseUrl: "https://api.turnkey.com",
apiPublicKey: process.env.API_PUBLIC_KEY!,
apiPrivateKey: process.env.API_PRIVATE_KEY!,
defaultOrganizationId: process.env.ORGANIZATION_ID!,
});
const activityWebhook = await turnkey.apiClient().createWebhookEndpoint({
type: "ACTIVITY_TYPE_CREATE_WEBHOOK_ENDPOINT",
timestampMs: Date.now().toString(),
organizationId: process.env.ORGANIZATION_ID!,
parameters: {
url: "https://example.com/webhooks/turnkey",
name: "Activity updates",
subscriptions: [{ eventType: "ACTIVITY_UPDATES" }],
},
});
For balance webhooks, use the same endpoint API with the balance event type:
const balanceWebhook = await turnkey.apiClient().createWebhookEndpoint({
type: "ACTIVITY_TYPE_CREATE_WEBHOOK_ENDPOINT",
timestampMs: Date.now().toString(),
organizationId: process.env.ORGANIZATION_ID!,
parameters: {
url: "https://example.com/webhooks/balances",
name: "Balance confirmations",
subscriptions: [{ eventType: "BALANCE_CONFIRMED_UPDATES" }],
},
});
The name field is required. Event types must be passed in subscriptions[]; do not pass a top-level eventTypes field.
Event types
| Event type | Description | Configuration scope |
|---|
ACTIVITY_UPDATES | Sends activity status updates. Parent-owned subscriptions receive parent and sub-organization activity events. Sub-organization-owned subscriptions receive only that sub-organization’s activity events. | Organization-scoped |
BALANCE_CONFIRMED_UPDATES | Sends confirmed balance update events for tracked wallet account addresses. | Billing organization / parent organization scoped |
Balance webhook endpoints must be managed from the billing organization. Sub-organization attempts to create, update, or delete balance-scoped endpoints return PermissionDenied.
Delivery contract
Turnkey sends each webhook as an HTTPS POST request. The request body is JSON and the Content-Type header is application/json. Your endpoint should return a 2xx status code after it accepts the delivery.
Only active endpoints and active subscriptions receive deliveries. Turnkey sets the canonical value for each X-Turnkey- header on every delivery.
Turnkey includes these headers on Webhooks V2 deliveries:
| Header | Description |
|---|
X-Turnkey-Organization-Id | Organization associated with the delivered event. |
X-Turnkey-Event-Type | Event type, such as ACTIVITY_UPDATES or BALANCE_CONFIRMED_UPDATES. |
X-Turnkey-Timestamp | Unix timestamp in milliseconds. For signed V2 deliveries, this is the delivery-attempt time. |
X-Turnkey-Webhook-Version | Webhook delivery contract version. The current value is 1. |
Signed Webhooks V2 deliveries also include:
| Header | Description |
|---|
X-Turnkey-Event-Id | Stable event identifier for this webhook event. |
X-Turnkey-Signature-Key-Id | Identifier for the Turnkey signing key. |
X-Turnkey-Signature-Algorithm | Signature algorithm. The current value is ed25519. |
X-Turnkey-Signature-Version | Signature contract version. The current value is v1. |
X-Turnkey-Signature | Hex-encoded Ed25519 signature. |
Turnkey treats 2xx responses as successful. Network errors and 5xx responses are retried. 3xx, 4xx, and 429 responses are terminal failures and are not retried. Redirects are not followed. Signed retries receive a fresh timestamp and signature, so deduplicate by the event or payload idempotency fields rather than by signature value.
Verify signatures
Verify the signature before parsing or trusting the webhook body. Signature verification requires the exact raw request body bytes that Turnkey sent. Re-serializing parsed JSON, changing whitespace, or changing key order causes verification to fail.
The signed message is:
v1.ed25519.<signing_key_id>.<timestamp_ms>.<event_id>.<raw_body>
Use the SDK helper to verify the headers, timestamp freshness, and raw body. The default freshness window is 5 minutes, so make sure your webhook receiver’s clock is synchronized.
import { verifyWebhookSignature } from "@turnkey/sdk-server/webhooks";
app.post(
"/webhooks/turnkey",
express.raw({ type: "application/json" }),
async (req, res) => {
const verified = await verifyWebhookSignature({
headers: req.headers,
rawBody: req.body,
verificationKeys: await loadTurnkeyWebhookVerificationKeys(),
});
if (!verified) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
// Process the event idempotently.
res.sendStatus(204);
},
);
Permissions
Creating, updating, and deleting webhook endpoints are standard Turnkey write activities. Root users can approve them by default. Use Turnkey policies to delegate webhook management to non-root users.
For example, to allow a non-root user to create webhook endpoints, create an allow policy with this condition:
activity.type == 'ACTIVITY_TYPE_CREATE_WEBHOOK_ENDPOINT'
Use the same pattern for updates and deletes:
activity.type == 'ACTIVITY_TYPE_UPDATE_WEBHOOK_ENDPOINT'
activity.type == 'ACTIVITY_TYPE_DELETE_WEBHOOK_ENDPOINT'
Read operations, such as listing webhook endpoints, use standard authenticated query access.
Manage endpoints
Use the webhook endpoint APIs to manage existing endpoints:
| Operation | Path | Notes |
|---|
| Create endpoint | /public/v1/submit/create_webhook_endpoint | Requires url, name, and subscriptions[]. |
| Update endpoint | /public/v1/submit/update_webhook_endpoint | Updates url, name, or isActive. |
| Delete endpoint | /public/v1/submit/delete_webhook_endpoint | Deletes an endpoint and its subscriptions. |
| List endpoints | /public/v1/query/list_webhook_endpoints | Returns endpoints and their subscriptions for an organization. |
Set isActive to false if you want to pause delivery without deleting the endpoint.
Payloads
ACTIVITY_UPDATES deliveries contain the external activity representation for the activity that changed. Use the activity id and the webhook X-Turnkey-Event-Id header to process deliveries idempotently.
BALANCE_CONFIRMED_UPDATES deliveries use this top-level shape:
{
"type": "balances:confirmed",
"msg": {
"operation": "deposit",
"caip2": "eip155:1",
"txHash": "0x...",
"address": "0x...",
"orgID": "<organization-id>",
"parentOrgID": "<billing-organization-id>",
"idempotencyKey": "<idempotency-key>",
"asset": {
"symbol": "ETH",
"name": "Ethereum",
"decimals": 18,
"caip19": "eip155:1/slip44:60",
"amount": "1000000000000000000"
},
"block": {
"number": 12345678,
"hash": "0x...",
"timestamp": "2026-05-05T12:34:56Z"
}
}
}
Troubleshooting
| Symptom | What to check |
|---|
createWebhookEndpoint is unavailable in your SDK | Upgrade to a Webhooks V2-capable SDK release. The minimum SDK version will be listed in the SDK changelog once published. |
PermissionDenied on create/update/delete | Confirm the user has a standard allow policy for the webhook activity type, and confirm balance webhooks are being managed from the billing organization. |
| Missing required field errors | Confirm parameters.name is set. |
| Subscription shape errors | Pass event types inside parameters.subscriptions[], not as top-level eventTypes. |
| Invalid webhook URL errors | Use an HTTPS URL that resolves to a public destination. Localhost, private IPs, link-local addresses, metadata endpoints, and URLs with user info are rejected. |
| Signature verification fails | Verify against the exact raw request body bytes, use the millisecond timestamp and event id from the headers, check clock skew, and select the public key matching the signature key id. |
Legacy activity webhooks
Legacy activity webhooks are configured with the FEATURE_NAME_WEBHOOK organization feature. They send activity notifications to a single URL and do not include the Webhooks V2 endpoint/subscription model or Webhooks V2 signature headers.
turnkey request --path /public/v1/submit/set_organization_feature --body '{
"timestampMs": "'"$(date +%s)"'000",
"type": "ACTIVITY_TYPE_SET_ORGANIZATION_FEATURE",
"organizationId": "'"$ORGANIZATION_ID"'",
"parameters": {
"name": "FEATURE_NAME_WEBHOOK",
"value": "'"$WEBHOOK_URL"'"
}
}' --key-name=$KEY_NAME