> ## Documentation Index
> Fetch the complete documentation index at: https://docs.base44.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Internal links on these pages omit the .md extension. Append .md to a docs page URL, or send an Accept: text/markdown header, to get that page as markdown.

# webhooks

***

## Overview

Webhooks module for receiving and verifying Base44's webhooks.

Base44 sends a signed event to your endpoint:

* When an app is created, published, unpublished, moved to trash, or restored.
* When you add the endpoint, to test it.

The module verifies each delivery's signature with your workspace's public keys and returns the
event with a typed payload. It runs on Node.js 20 and later and on edge runtimes, and makes
no network requests.

Use [`createHandler()`](#createhandler) for a ready-made route handler, or
[`verifyEvent()`](#verifyevent) to handle the request yourself. Base44 delivers each event at
least once and in no guaranteed order, so skip events whose `id` you already handled.

## Methods

### verifyEvent()

> **verifyEvent**(`body`, `headers`, `options`): `Promise`\<`WebhookEvent`>

Verifies a delivery and returns its event.

Pass the request body exactly as received, before any JSON parsing, because the signature covers
its exact bytes. Nothing is parsed until the signature checks out.

Unlike [`createHandler()`](#createhandler), `verifyEvent()` doesn't answer the request for
you. When you add an endpoint, Base44 sends it a `webhook.test.v1` event and activates the endpoint
only if you answer with `{ "challenge": event.data.challenge }` within 5 seconds. Answer every
other event with a 2xx, or Base44 retries it.

#### Parameters

<Accordion title="Properties">
  <ParamField body="body" type="any" required>
    The raw request body, as text or bytes.

    `string` | `Uint8Array`\<`ArrayBufferLike`>
  </ParamField>

  <ParamField body="headers" type="WebhookHeaders" required>
    The request headers.
  </ParamField>

  <ParamField body="options" type="VerifyEventOptions" required>
    Verification options, including your public keys.
  </ParamField>

  <Accordion title="Properties">
    <ParamField body="publicKeys" type="WebhookPublicKeys" required>
      Your workspace's public keys.

      Pass both the old and the new key while a key rotation is in progress. Missing or malformed keys
      reject with `WebhookConfigurationError`.
    </ParamField>

    <ParamField body="toleranceSeconds" type="number">
      How far, in seconds, a delivery's timestamp can be from your server's clock. Defaults to `300`.
    </ParamField>
  </Accordion>
</Accordion>

#### Returns

[`WebhookEvent`](#webhookevent)

A promise that resolves to the verified event.

#### Throws

* The promise rejects with a `WebhookVerificationError` when the delivery can't be trusted. Answer with a 400.
* The promise rejects with a `WebhookConfigurationError` when the public keys are missing or malformed, or the runtime can't use them. Answer with a 500.

#### Example

<CodeGroup>
  ```typescript Verify the request and handle each event type theme={null}
  export async function POST(request) {
    const rawBody = await request.text();
    let event;
    try {
      event = await platform.webhooks.verifyEvent(rawBody, request.headers, {
        publicKeys: process.env.BASE44_WEBHOOK_PUBLIC_KEYS,
      });
    } catch (error) {
      if (error instanceof WebhookVerificationError) return new Response(null, { status: 400 });
      throw error;
    }

    switch (event.type) {
      case 'webhook.test.v1':
        // Answer the test event so Base44 activates the endpoint
        return Response.json({ challenge: event.data.challenge });
      case 'app.deleted.v1':
        await removeApp(event.data.app_id);
        break;
    }
    return new Response(null, { status: 204 });
  }
  ```
</CodeGroup>

***

### createHandler()

> **createHandler**(`options`): (`request`) => `Promise`\<`Response`>

Creates a route handler that verifies deliveries and calls your handlers.

The returned function takes a standard Fetch API `Request` and returns a `Response`, so you can
use it with any framework built on the Fetch API, such as Next.js route handlers, Hono,
Cloudflare Workers, Deno, and Bun. For each request, it reads the raw body and
verifies it. It answers the test event itself, and for every other event it calls `onEvent()`,
then the event's handler in `on`, then `onSuccess()`.

It answers with:

* **204**: Event was handled, including events with no handler and unknown events.
* **400**: Delivery doesn't verify. Body carries the error code.
* **500**: Public keys are misconfigured (`receiver_misconfigured`), body can't be read
  (`request_failed`), or a callback throws (`handler_failed`).

Base44 retries every answer that isn't 2xx, so it retries both 400 and 500. The two codes only
differ in Base44's delivery log, where a 400 points to the delivery and a 500 points to your server.

Base44 might deliver the same event more than once, so make your handlers safe to run twice for the
same `event.id`.

#### Parameters

<ParamField body="options" type="CreateHandlerOptions" required>
  Your public keys, handlers, and callbacks.
</ParamField>

<Accordion title="Properties">
  <ParamField body="publicKeys" type="WebhookPublicKeys" required>
    Your workspace's public keys.

    Pass both the old and the new key while a key rotation is in progress. Missing or malformed keys
    reject with `WebhookConfigurationError`.
  </ParamField>

  <ParamField body="toleranceSeconds" type="number">
    How far, in seconds, a delivery's timestamp can be from your server's clock. Defaults to `300`.
  </ParamField>

  <ParamField body="on" type="WebhookEventHandlers">
    Handlers keyed by event type. Events without a handler, including unknown ones, are still answered 2xx, so Base44 doesn't retry them.
  </ParamField>

  <ParamField body="onEvent" type="(event: DeliveredWebhookEvent) => unknown">
    Called for every verified event, before its handler in `on`.
  </ParamField>

  <ParamField body="onSuccess" type="(event: DeliveredWebhookEvent) => void | Response | Promise<void | Response>">
    Called after `onEvent()` and the event's handler finish without throwing.

    Return a `Response` to replace the default empty 204.
  </ParamField>

  <ParamField body="onError" type="(error: unknown, event: DeliveredWebhookEvent) => void | Response | Promise<void | Response>">
    Called when a delivery fails.

    The handler answers a `WebhookVerificationError` with a 400, and a `WebhookConfigurationError`, a
    failed body read, or anything a callback throws with a 500. Base44 retries both, because it retries
    every answer that isn't 2xx. The two codes only differ in Base44's delivery log, where a 400
    points to the delivery and a 500 points to your server. The `event` argument is set when the
    delivery verified and a callback failed.
    Return a `Response` to replace the default answer.
  </ParamField>
</Accordion>

#### Returns

A function that takes a `Request` and resolves to the `Response` to send.

> (`request`): `Promise`\<`Response`>

<Accordion title="Properties">
  <ResponseField name="Parameters" type="any" required>
    <ParamField body="request" type="Request" required />
  </ResponseField>

  <ResponseField name="Returns" type="Promise" required />
</Accordion>

#### Example

<CodeGroup>
  ```typescript Handle app events in a Next.js route theme={null}
  export const POST = platform.webhooks.createHandler({
    publicKeys: process.env.BASE44_WEBHOOK_PUBLIC_KEYS,
    on: {
      'app.deleted.v1': async (event) => removeApp(event.data.app_id),
      'app.published.v1': async (event) => refreshApp(event.data.app_id),
    },
    onError(error, event) {
      console.error('Webhook failed', event?.id, error);
    },
  });
  ```
</CodeGroup>

## Type Definitions

### WebhookEvent

> **WebhookEvent** = `{ [K in WebhookEventType]: CloudEvent<K, WebhookEventMap[K]> }`\[`WebhookEventType`] | `UnknownWebhookEvent`

A verified webhook event.

Check `type` to narrow `data` to that event's payload. An event added to Base44 after this version
of the SDK arrives with `type: "unknown"`, its type in `name`, and an unchecked `data`, so check a
field before using it. After you upgrade, the event arrives under its own `type`. Answer it with a
2xx too, because Base44 retries any answer that isn't 2xx.

#### Example

<CodeGroup>
  ```typescript Narrow an event by type theme={null}
  switch (event.type) {
    case 'app.deleted.v1':
      await removeApp(event.data.app_id);
      break;
    case 'unknown':
      console.log(`Skipping ${event.name}, which this SDK version doesn't know`);
      break;
  }
  ```
</CodeGroup>

### UnknownWebhookEvent

A webhook event whose type this version of the SDK doesn't know, such as one Base44 added later.

The envelope is the same as for every event, but `type` is `"unknown"`, the event's real type is
in `name`, and `data` isn't checked, so check a field before using it.

<ResponseField name="specversion" type="'1.0'" required>
  CloudEvents version. Always `1.0`.
</ResponseField>

<ResponseField name="id" type="string" required>
  Unique event ID, starting with `evt_`. A redelivery keeps the same ID, so use it to skip events you already handled.
</ResponseField>

<ResponseField name="source" type="string" required>
  URL of the workspace the event happened in.
</ResponseField>

<ResponseField name="subject" type="string" required>
  The resource the event is about, such as `apps/<app ID>`.
</ResponseField>

<ResponseField name="time" type="string" required>
  When the event happened, as an RFC 3339 timestamp in UTC.
</ResponseField>

<ResponseField name="datacontenttype" type="string" required>
  Content type of `data`. Always `application/json`.
</ResponseField>

<ResponseField name="dataschema" type="string" required>
  URL of the JSON Schema that describes `data`.
</ResponseField>

<ResponseField name="traceparent" type="string">
  W3C trace context, when the event was produced inside a trace.
</ResponseField>

<ResponseField name="tracestate" type="string">
  W3C trace state, when the event was produced inside a trace.
</ResponseField>

<ResponseField name="authtype" type="string" required>
  Kind of actor that caused the event.
</ResponseField>

<ResponseField name="authid" type="string" required>
  ID of the actor that caused the event.
</ResponseField>

<ResponseField name="data" type="TData" required>
  The event's payload. Its shape depends on `type`.
</ResponseField>

<ResponseField name="type" type="'unknown'" required>
  Always `"unknown"`. Narrows the event to this shape.
</ResponseField>

<ResponseField name="name" type="string" required>
  The event's type as Base44 sent it, such as `app.renamed.v1`.
</ResponseField>

## Errors

### WebhookVerificationError

The error the SDK reports when a webhook delivery can't be trusted.

You don't create these errors. The [`verifyEvent()`](/developers/references/platform-sdk/docs/interfaces/webhooks#verifyevent) method rejects with
them, and [`createHandler()`](/developers/references/platform-sdk/docs/interfaces/webhooks#createhandler) answers them with a 400 and passes
them to your `onError()`. Check `code` to see what happened, then look the code up below.

These errors never carry the request body or its signature.

#### Example

<CodeGroup>
  ```typescript Refuse a delivery that doesn't verify theme={null}
  try {
    const event = await platform.webhooks.verifyEvent(rawBody, request.headers, { publicKeys });
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      return Response.json({ error: error.code }, { status: 400 });
    }
    throw error;
  }
  ```
</CodeGroup>

<ResponseField name="code" type="WebhookVerificationErrorCode" required>
  What happened. One of the verification error codes.
</ResponseField>

<Accordion title="Verification error codes">
  Codes of errors for a delivery that can't be trusted. Answer these with a 400. Base44 retries any
  answer that isn't 2xx, so a genuine delivery that failed because of a passing problem verifies on the retry.

  <ResponseField name="missing_headers">
    The request is missing one of the `webhook-id`, `webhook-timestamp`, or `webhook-signature` headers.

    Check that a proxy in front of your endpoint forwards all request headers.
  </ResponseField>

  <ResponseField name="timestamp_outside_tolerance">
    The `webhook-timestamp` header isn't a number, or is further from your server's clock than `toleranceSeconds`, which defaults to 300.

    Check your server's clock. A replayed old delivery fails this way too.
  </ResponseField>

  <ResponseField name="no_signature_entries">
    The `webhook-signature` header has no signature in the `v1a` scheme Base44 uses.

    The request didn't come from Base44, or a proxy rewrote the header.
  </ResponseField>

  <ResponseField name="signature_mismatch">
    No signature matches any of your public keys.

    Check that you pass the request body exactly as received. Parsing the JSON and serializing it again
    changes the bytes. Also check that your public keys belong to the workspace that sends the webhook.
  </ResponseField>

  <ResponseField name="invalid_payload">
    The signature is valid, but the body isn't a JSON event.

    This shouldn't happen with deliveries from Base44.
  </ResponseField>
</Accordion>

### WebhookConfigurationError

The error the SDK reports when the public keys you passed can't be used, so no delivery can be verified.

You don't create these errors. The [`verifyEvent()`](/developers/references/platform-sdk/docs/interfaces/webhooks#verifyevent) method rejects with
them, and [`createHandler()`](/developers/references/platform-sdk/docs/interfaces/webhooks#createhandler) answers them with a 500 and passes
them to your `onError()`. Base44 retries the delivery, and the retries succeed once you fix the problem.

#### Example

<CodeGroup>
  ```typescript Log a misconfigured deployment, and let it answer 500 theme={null}
  export const POST = platform.webhooks.createHandler({
    publicKeys: process.env.BASE44_WEBHOOK_PUBLIC_KEYS,
    onError(error) {
      if (error instanceof WebhookConfigurationError) console.error('Check BASE44_WEBHOOK_PUBLIC_KEYS', error.code);
    },
  });
  ```
</CodeGroup>

<ResponseField name="code" type="WebhookConfigurationErrorCode" required>
  What's wrong with the keys. One of the configuration error codes.
</ResponseField>

<Accordion title="Configuration error codes">
  Codes of errors that stop verification before it starts, because the public keys you passed can't
  be used. They're about your deployment, not the request, so answer them with a 500. Base44 retries
  the delivery, as it does for any answer that isn't 2xx, and the retry succeeds once you fix the problem.

  <ResponseField name="missing_public_keys">
    No public keys were passed, for example because an environment variable is unset.

    Pass your workspace's public keys to `verifyEvent()` or `createHandler()`.
  </ResponseField>

  <ResponseField name="invalid_public_key">
    A public key isn't a base64-encoded 32-byte Ed25519 key, with or without the `whpk_` prefix.

    Copy the key again. A truncated key fails this way.
  </ResponseField>

  <ResponseField name="unsupported_runtime">
    The runtime can't import Ed25519 keys with WebCrypto.

    Run your server on Node.js 20 or later, or on an edge runtime that supports Ed25519 in WebCrypto.
  </ResponseField>
</Accordion>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.