Postbote
Guides

Write an Adapter

Build a provider integration that follows the same behavioral contract as official adapters.

Use defineAdapter() and the adapter contract suite for a custom provider.

Terminal
pnpm add @postbote/core
pnpm add -D @postbote/adapter-contract vitest

Your adapter should map provider responses into a message ID and normalized errors. Never include request headers or credentials in PostboteError.cause.

src/adapters/acme-mail.ts
import {
  defineAdapter,
  httpStatusToErrorCode,
  PostboteError,
} from "@postbote/core";

export const acmeMail = (apiKey: string) =>
  defineAdapter({
    name: "acme-mail",
    async send(message, { signal }) {
      const response = await fetch("https://mail.acme.test/send", {
        method: "POST",
        headers: { Authorization: `Bearer ${apiKey}` },
        body: JSON.stringify(message),
        signal,
      });
      const body = await response.json().catch(() => undefined);

      if (!response.ok) {
        throw new PostboteError(response.statusText, {
          code: httpStatusToErrorCode(response.status),
          provider: "acme-mail",
          cause: { status: response.status, body },
        });
      }

      return { messageId: body.id, raw: body };
    },
  });

Then call runAdapterContractTests() from one contract test file. The suite checks error codes, retryability, message IDs, abort propagation, and credential leakage. Use skip only for a provider behavior that cannot be generated synchronously.