Webhook Adapter API
A webhook adapter is a small TypeScript module. It registers a webhook with a provider, verifies each incoming request, and changes requests into events. Type hosts the callback URL. For a guide, see Receive Webhooks from Other Services.
Slack and GitHub do not need an adapter. Type receives their events for you.
Ownership
Section titled “Ownership”- A connection owns the webhook registration. An automation subscribes to it.
- Two automations that use the same connection and the same webhook key share one registration.
- An integration manager installs and maintains the adapter. Automation code cannot manage adapters.
- Type pins the registration to the credential account of the connection. Type never moves it to a different account.
Module exports
Section titled “Module exports”An adapter file exports these values:
| Export | Required | Meaning |
|---|---|---|
default | Yes | An object that satisfies IntegrationWebhook. |
events | Yes | An array of event schemas. Make each one with webhookEvent(). |
fixtures | Yes | 2 to 20 receive fixtures. At least one must accept an event, and at least one must reject a request. |
settings | No | Non-secret JSON settings. The default is {}. |
Add the adapter to the deployment export of the automation:
import type { AutomationDeployment } from "@type-dot-com/sdk/automations";import { meetingCompleted } from "./webhook";
export const deployment = { webhooks: { meetings: { connectionId: "orgint_abc123", entrypoint: "src/webhook.ts" }, }, subscriptions: { meetingReady: { title: "Log a completed meeting", description: "Record each completed meeting", source: { kind: "integration", connectionId: "orgint_abc123", webhook: "meetings", events: ["meeting.completed"], }, schema: meetingCompleted, }, },} satisfies AutomationDeployment;The webhook key (meetings) names the registration on the connection. The subscription key (meetingReady) names the method. They do not need to match.
A webhook key starts with a lowercase letter. It can have lowercase letters, digits, _, and -. The maximum length is 64 characters.
webhookEvent()
Section titled “webhookEvent()”function webhookEvent<Type extends string, Data extends ZodType>(type: Type, data: Data): ZodObject<{ id: ZodString; type: ZodLiteral<Type>; resourceId: ZodOptional<ZodNullable<ZodString>>; resourceType: ZodOptional<ZodNullable<ZodString>>; data: Data;}>;Defines one event shape. Use the same schema in the adapter events export and in the subscription schema.
import { webhookEvent } from "@type-dot-com/sdk/webhooks";import { z } from "zod";
export const meetingCompleted = webhookEvent( "meeting.completed", z.object({ id: z.string().min(1), title: z.string(), summary: z.string().nullable() }),);export const events = [meetingCompleted];IntegrationWebhook
Section titled “IntegrationWebhook”interface IntegrationWebhook { setup(ctx: WebhookSetupContext): Result<WebhookRegistration, Error> | PromiseLike<...>; receive(ctx: WebhookReceiveContext, request: WebhookRequest): Result<WebhookReceiveResult, Error> | PromiseLike<...>; remove(ctx: WebhookSetupContext): Result<void, Error> | PromiseLike<...>; renew?(ctx: WebhookSetupContext): Result<WebhookRegistration, Error> | PromiseLike<...>;}| Method | When Type calls it | Return |
|---|---|---|
setup | At first activation and at recovery. | { id, state, renewAt? } |
receive | For each incoming GET or POST. | Events, a challenge response, or a rejection. |
remove | When a manager removes the registration, or when a connection is disconnected. | ok(undefined), also when the registration is already gone. |
renew | Optional. When renewAt comes, or when a manager asks. | The updated registration with its next renewAt. |
Return err(error) when a method cannot get a successful result.
WebhookRegistration
Section titled “WebhookRegistration”interface WebhookRegistration { id: string; // 1 to 500 characters state: JsonValue; // encrypted at rest renewAt?: string; // ISO date-time, at least 1 minute in the future}Put the data that later calls need in state, for example a provider signing key or a lease ID. Type encrypts state. Management responses never show it.
WebhookReceiveContext
Section titled “WebhookReceiveContext”| Field | Meaning |
|---|---|
now | The host time, as an ISO string. Use it for timestamp checks. |
settings | The non-secret settings of the adapter. |
secret | A random signing key that Type makes and saves before setup. Do not log it. |
registration | The last saved registration, or null. |
crypto | Cryptographic helpers. See below. |
receive has no provider API, no network, no file system, and no agent access.
WebhookSetupContext
Section titled “WebhookSetupContext”Has all fields of WebhookReceiveContext, and these:
| Field or method | Meaning |
|---|---|
url | The callback URL to register with the provider. |
events | The event types that the adapter declares. |
operationId | For setup: the same value for all attempts on this registration, also after code repairs. For other methods: a new value for each call. Use it as an idempotency key. |
request({ method, path, query?, headers?, body? }) | An authorized request to the connection’s API. path is relative to the base URL. Returns { status, data }. |
save(registration) | Saves the registration before the next external step. A challenge can then use the saved state. |
Check status after each request. A non-success status does not prove that an operation failed or succeeded.
WebhookRequest
Section titled “WebhookRequest”interface WebhookRequest { method: "GET" | "POST"; headers: Record<string, string>; // lowercase names query: Record<string, string>; rawBody: string; // the unchanged UTF-8 body, at most 1 MiB}Verify the signature against rawBody before you parse it.
WebhookReceiveResult
Section titled “WebhookReceiveResult”Return exactly one of these:
// An authenticated delivery. 0 to 100 events.ok({ kind: "events", events: [{ id, type, resourceType, resourceId, data }] });
// A verified challenge. No automation runs.ok({ kind: "respond", status: 200, body: challenge, contentType: "text/plain" });
// A request that failed authentication. Type records no event.ok({ kind: "reject", status: 401 });| Kind | Rules |
|---|---|
events | At most 100 events. An empty list accepts the request and starts no work. |
respond | status is 200, 202, or 204. body is a string of at most 8,192 characters. contentType is text/plain (default) or application/json. Encode JSON as a string. |
reject | status is 400, 401, or 403. |
Each event has these fields:
| Field | Rules |
|---|---|
id | 1 to 300 characters. Use the provider’s event ID. It must stay the same when the provider retries. A random ID stops retry deduplication. |
type | 1 to 200 characters. One of the adapter’s declared events. |
resourceType, resourceId | Optional, can be null. Both must be present to match a resources filter. |
data | JSON. |
crypto
Section titled “crypto”interface WebhookCrypto { hmac(input: { algorithm: "sha256" | "sha1" | "sha512"; key: string; keyEncoding?: "utf8" | "base64" | "hex"; data: string; encoding: "hex" | "base64" }): string; digest(input: { algorithm: "sha256" | "sha512"; data: string; encoding: "hex" | "base64" }): string; equal(left: string, right: string): boolean; // constant-time comparison verify(input: { algorithm: "sha256" | "sha512" | "ed25519"; publicKey: string; data: string; signature: string; encoding: "hex" | "base64" }): boolean;}Use equal to compare signatures. Do not use ===.
Fixtures
Section titled “Fixtures”Each adapter needs 2 to 20 fixtures. Type runs them when you deploy. Fixtures cannot call live APIs.
interface WebhookFixture { name: string; // 1 to 200 characters request: WebhookRequest; secret: string; // a fake key, at most 4,096 characters registration: WebhookRegistration | null; now: string; // a fixed ISO date-time expected: WebhookReceiveResult; // the exact result}Add fixtures for a forged signature, changed body bytes, an old timestamp, missing required fields, and each provider challenge.
Status values
Section titled “Status values”| Status | Meaning |
|---|---|
pending | Saved, not yet set up. |
creating | setup is in progress. |
ready | The registration accepts events. |
renewing | renew is in progress. |
removing | remove is in progress. |
needs_attention | A lifecycle call failed or expired, or the credential changed. A manager must act. |
removed | The key is retired. Use a new key to register again. |
Management operations
Section titled “Management operations”Integration managers use the manage_integration_webhooks tool in Type, or type.integrations.webhooks() in the SDK. Every request has connectionId. Operations that change a registration also need key and expectedRevision.
| Operation | Extra fields | Result |
|---|---|---|
list | — | Summaries for the connection. |
get | key | The summary, source, definition, and fixtures. |
test | definition, codeFiles | Typecheck and run fixtures. Changes nothing. |
replace | key, expectedRevision, definition, codeFiles | Publish a tested code repair. Events and settings must not change. |
setup | key, expectedRevision | Create or recover the same registration. |
renew | key, expectedRevision | Renew a ready registration now. |
remove | key, expectedRevision | Remove a registration that has no subscriptions. |
If expectedRevision is not the current revision, the operation fails with a conflict. Read the registration again, then decide.
Behavior and guarantees
Section titled “Behavior and guarantees”- Before ready: Type answers incoming events with
503until the registration is ready and the first subscription is active. Type does not keep events that come before this point. - Delivery: For each verified event, Type saves the event and its active subscribers before it answers
202. A retry with the same event ID uses the saved subscriber list. - New subscribers: A subscriber that you add later does not get older events.
- Pause: Events that come while a subscriber is paused are not saved for that subscriber. Resume does not replay them.
- Credential change: If someone reconnects or changes the credential, the webhook stops delivery until a manager runs
setup. For a personal account, only its owner can do this. - Renewal: Type checks for due renewals every minute, 20 registrations at a time. Set
renewAtwell before the provider expiry. - Disconnect: Type tries to remove the provider registration, then deletes the credential even if removal fails. You can have to remove the provider registration by hand.
Limits
Section titled “Limits”| Item | Limit |
|---|---|
| Incoming request | GET or POST. UTF-8 body up to 1 MiB. |
receive call | 5 seconds, including startup. 32 MiB VM memory. |
setup, renew, remove call | 60 seconds. |
Provider requests and save calls | 32 for each lifecycle call. |
| Provider response | 1 MiB. Type rejects a truncated response. |
| Registration state or call result | 64 KiB of JSON. |
| Events per request | 100. |
| Challenge body | 8,192 characters. |
| Incoming rate | 120 requests per minute for each workspace. More requests get 429. |
| Adapters per automation request | 20. |
| Fixtures per adapter | 2 to 20. |