Skip to content
Changelog

Report a bug

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.

  • 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.

An adapter file exports these values:

ExportRequiredMeaning
defaultYesAn object that satisfies IntegrationWebhook.
eventsYesAn array of event schemas. Make each one with webhookEvent().
fixturesYes2 to 20 receive fixtures. At least one must accept an event, and at least one must reject a request.
settingsNoNon-secret JSON settings. The default is {}.

Add the adapter to the deployment export of the automation:

src/main.ts
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.

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];
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<...>;
}
MethodWhen Type calls itReturn
setupAt first activation and at recovery.{ id, state, renewAt? }
receiveFor each incoming GET or POST.Events, a challenge response, or a rejection.
removeWhen a manager removes the registration, or when a connection is disconnected.ok(undefined), also when the registration is already gone.
renewOptional. 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.

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.

FieldMeaning
nowThe host time, as an ISO string. Use it for timestamp checks.
settingsThe non-secret settings of the adapter.
secretA random signing key that Type makes and saves before setup. Do not log it.
registrationThe last saved registration, or null.
cryptoCryptographic helpers. See below.

receive has no provider API, no network, no file system, and no agent access.

Has all fields of WebhookReceiveContext, and these:

Field or methodMeaning
urlThe callback URL to register with the provider.
eventsThe event types that the adapter declares.
operationIdFor 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.

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.

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 });
KindRules
eventsAt most 100 events. An empty list accepts the request and starts no work.
respondstatus 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.
rejectstatus is 400, 401, or 403.

Each event has these fields:

FieldRules
id1 to 300 characters. Use the provider’s event ID. It must stay the same when the provider retries. A random ID stops retry deduplication.
type1 to 200 characters. One of the adapter’s declared events.
resourceType, resourceIdOptional, can be null. Both must be present to match a resources filter.
dataJSON.
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 ===.

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.

StatusMeaning
pendingSaved, not yet set up.
creatingsetup is in progress.
readyThe registration accepts events.
renewingrenew is in progress.
removingremove is in progress.
needs_attentionA lifecycle call failed or expired, or the credential changed. A manager must act.
removedThe key is retired. Use a new key to register again.

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.

OperationExtra fieldsResult
list—Summaries for the connection.
getkeyThe summary, source, definition, and fixtures.
testdefinition, codeFilesTypecheck and run fixtures. Changes nothing.
replacekey, expectedRevision, definition, codeFilesPublish a tested code repair. Events and settings must not change.
setupkey, expectedRevisionCreate or recover the same registration.
renewkey, expectedRevisionRenew a ready registration now.
removekey, expectedRevisionRemove 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.

  • Before ready: Type answers incoming events with 503 until 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 renewAt well 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.
ItemLimit
Incoming requestGET or POST. UTF-8 body up to 1 MiB.
receive call5 seconds, including startup. 32 MiB VM memory.
setup, renew, remove call60 seconds.
Provider requests and save calls32 for each lifecycle call.
Provider response1 MiB. Type rejects a truncated response.
Registration state or call result64 KiB of JSON.
Events per request100.
Challenge body8,192 characters.
Incoming rate120 requests per minute for each workspace. More requests get 429.
Adapters per automation request20.
Fixtures per adapter2 to 20.