External package invocation API
September 10, 2026 · View on GitHub
Inbound webhooks are the advertised external HTTP knock
(inputMode: "params", Idempotency-Key, minted handle, no Bearer). This page
documents the unadvertised invocation-token drain that leftover callers still
hit.
A stable webhook proxy is a representative leftover caller:
- the external proxy owns the provider-specific connection or webhook ingress
- the proxy normalizes inbound events
- the proxy calls this drain
- Kody executes the saved package export with package context, user context, package storage, and normal secret/capability rules
Kody is the package runtime and storage brain. The external service owns the provider lifecycle. New first-party callers mint a webhook handle instead.
Endpoint
POST /@:username/api/package-invocations/:kodyId/:exportName
Examples:
/@alice/api/package-invocations/webhook-dispatcher/dispatch-event
The path uses the owner's username and the package name leaf.
The export name is normalized to package export form, so dispatch-event
resolves as ./dispatch-event.
Authentication
Authentication uses a private bearer token stored in Kody's database-backed package invocation token table.
Kody stores only the token hash for request-time lookup. The raw bearer token is sent by the leftover external service as:
Authorization: Bearer <raw-token>
Each token belongs to exactly one saved package. Auth resolves the owner and
package from the URL, then looks up (user_id, package_id, token_hash).
Each token row includes:
- token id and human-readable name
- owning
user_idandpackage_id - allowed package exports (
export_names_json) last_used_atrevoked_at
The token is not a global backdoor:
- the path names the owner and package
- the bearer proves access to that package only
- export access requires an explicit allowlist (including per-package
*) - request JSON
sourceis an optional log label and does not gate auth - tokens can be revoked without deploys
- execution uses normal package runtime machinery
last_used_atis a best-effort write and does not gate authentication
Operator drain
Token list/create/rotate/revoke/delete stay on the same-origin JSON endpoint as an unadvertised operator drain. They are not in account navigation, package settings, or MCP search.
GET /account/packages.json— list packages; includeselected=<packageId>to load that package's token metadataPOST /account/packages.jsonwithaction: "create-token"— create a token for one owned packagePOST /account/packages.jsonwithaction: "update-token"— update token name, export allowlist, and optionally replace the stored token hash from a new raw tokenPOST /account/packages.jsonwithaction: "revoke-token"— revoke a token by idPOST /account/packages.jsonwithaction: "reinstate-token"— reinstate a revoked token by idPOST /account/packages.jsonwithaction: "delete-token"— permanently delete a token row by id
Create payload shape:
{
"action": "create-token",
"packageId": "<saved-package-id>",
"name": "Trusted external client",
"rawToken": "<raw-token>",
"exportNames": ["*"]
}
Export allowlists support the deliberate wildcard value * for every export on
that package. Token list/detail payloads never return the raw token or token
hash.
packageInvocationTokenList and packageInvocationTokenGet stay callable by
exact capability name as the same unadvertised drain. Search, domain listings,
and packageGet do not advertise tokens.
Request body
{
"params": {
"eventId": "123",
"content": "hello"
},
"idempotencyKey": "webhook:event:123",
"source": "webhook-dispatcher",
"topic": "webhook.event"
}
Fields:
params— JSON object passed as the first argument to the package exportidempotencyKey— required stable key for replay protectionsource— optional caller label for logs. It does not gate authentication or determine idempotency.topic— optional event topic label for downstream logic and logs
Idempotency
Kody persists package invocation idempotency in D1.
The identity key is:
- user
- token id
- package id
- export name
- idempotency key
Behavior:
- same request + same idempotency key => stored response replayed
- same idempotency key + different payload =>
409 idempotency_mismatch - duplicate while first invocation is still active =>
409 invocation_in_progress
This makes duplicate event deliveries safe when the proxy retries.
Response shape
Success:
{
"ok": true,
"package": {
"id": "pkg_123",
"kodyId": "webhook-dispatcher"
},
"exportName": "./dispatch-event",
"source": "webhook-dispatcher",
"topic": "webhook.event",
"idempotency": {
"key": "webhook:event:123",
"replayed": false
},
"result": {
"reply": "handled"
},
"logs": []
}
Replay responses return the same stored body with idempotency.replayed: true.
Failures return:
{
"ok": false,
"error": {
"code": "package_not_found",
"message": "Saved package \"webhook-dispatcher\" was not found for this user."
}
}
Execution failures return sanitized structured errors and logs. The route does not expose Worker secrets directly.
Runtime behavior
The API reuses the existing published package bundle path:
- resolve the saved package for the configured user
- resolve the requested package export
- load the published
moduleartifact if present - rebuild and persist the module artifact on cache miss
- execute through
runBundledModuleWithRegistry
Execution includes:
- package context
- repo context when a published source exists
- writable package storage bound to
package:{encodeURIComponent(packageId)}(packageStorage()) - user context from the scoped token config
Rate limiting and auditing
The endpoint is shaped for standard Cloudflare edge rate limiting:
- path is stable and narrow
- caller metadata includes
sourceandtopic - each request is audit-logged with hashed email/IP metadata
Prefer Cloudflare WAF/rate limiting rules in front of this path rather than adding bespoke in-Worker rate limiting first.
Leftover caller pattern
Existing drain callers POST with Authorization: Bearer and a stable
idempotencyKey:
curl --fail --silent \
-X POST \
-H "Authorization: Bearer $PACKAGE_INVOCATION_TOKEN" \
-H "Content-Type: application/json" \
"https://kody.example.com/@alice/api/package-invocations/webhook-dispatcher/dispatch-event" \
-d '{
"params": {
"eventId": "123",
"resourceId": "456",
"content": "hello"
},
"idempotencyKey": "webhook:event:123",
"source": "webhook-dispatcher",
"topic": "webhook.event"
}'
New first-party callers declare one webhook per export (inputMode: "params"),
mint a handle with webhookUrlMint, and send Idempotency-Key. There is no *
webhook. See Inbound webhooks.