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

# Webhooks

> Receive formation and document events, and verify their signatures.

Formation is asynchronous. Rather than polling, subscribe to webhook events to learn when a formation progresses and when documents become available.

## Setup

<Steps>
  <Step title="Get your signing secret">
    doola provisions your tenant and generates your webhook signing secret. It is shown once at creation, so store it securely. Email [engineering@doola.com](mailto:engineering@doola.com) to get set up or to rotate it.
  </Step>

  <Step title="Configure your endpoint">
    Set your webhook URL in the doola Partner Portal. The URL must be HTTPS. Each environment has its own tenant, so configure sandbox and production separately.
  </Step>
</Steps>

<Tip>
  To see real deliveries end to end without waiting on an actual formation, use the [Sandbox playground](/api/sandbox-playground): it completes the formation and EIN milestones on demand and fires the events below through the normal pipeline.
</Tip>

## Verifying the signature

doola sends a `POST` with an `X-Doola-Signature` header: an HMAC SHA256 hex digest of the raw request body, keyed with your webhook secret. (HTTP header names are case-insensitive; many frameworks lowercase them to `x-doola-signature` on the way in.)

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhook(rawBody, signature, webhookSecret) {
    const expected = crypto
      .createHmac('sha256', webhookSecret)
      .update(rawBody)
      .digest('hex');
    const expectedBuffer = Buffer.from(expected);
    const signatureBuffer = Buffer.from(signature ?? '');
    // timingSafeEqual throws if the buffers differ in length, so reject that first.
    if (expectedBuffer.length !== signatureBuffer.length) {
      return false;
    }
    return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib

  def verify_webhook(raw_body: bytes, signature: str, webhook_secret: str) -> bool:
      expected = hmac.new(
          webhook_secret.encode(), raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```
</CodeGroup>

<Warning>
  Always use a constant time comparison (`timingSafeEqual` or `compare_digest`). Plain string equality is vulnerable to timing attacks.
</Warning>

## Payload

Every delivery is a `POST` with the same envelope. Only `eventPayload` changes shape from event to event.

```json theme={null}
{
  "eventId": "31pLdpvMh4OfO90moxLgLT8AuQr",
  "eventName": "document_aoo_uploaded",
  "eventPayload": {
    "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
    "documentId": "31pLdj8SKAU9BYNchcohyto2AxC",
    "documentType": "ArticlesOfOrganization",
    "documentCreatedAt": "2025-08-26 13:49:16.0"
  },
  "timestamp": 1756216157000
}
```

| Field          | Type           | Description                                                                                      |
| -------------- | -------------- | ------------------------------------------------------------------------------------------------ |
| `eventId`      | string         | Unique ID for this event. Use it to dedupe redeliveries.                                         |
| `eventName`    | string         | Which event fired (see the table below).                                                         |
| `eventPayload` | object \| null | Event-specific body. Shape depends on `eventName`, and is `null` for `partner_webhook_disabled`. |
| `timestamp`    | number         | When the event fired, Unix epoch in **milliseconds**.                                            |

## Events

| Event                                  | Description                                                            |
| -------------------------------------- | ---------------------------------------------------------------------- |
| `company_formation_submitted`          | Formation accepted and processing started                              |
| `company_formation_completed`          | Formation complete, company is active                                  |
| `company_formation_failed`             | Formation failed after all retries; inspect `reasonCode` and `message` |
| `company_filing_date_updated`          | Filing date was updated by doola                                       |
| `company_ein_issued`                   | The company's EIN was issued; fetch the company to read `ein`          |
| `document_aoo_uploaded`                | Articles of Organization ready to download                             |
| `document_einletter_uploaded`          | EIN Letter ready to download                                           |
| `document_mail_uploaded`               | Business mail document uploaded                                        |
| `document_corporatebylaws_uploaded`    | Corporate Bylaws ready to download (CCorp only)                        |
| `document_operatingagreement_uploaded` | Operating Agreement ready to download (LLC only)                       |
| `signature_ss4_completed`              | A non-US founder signed their SS-4                                     |
| `document_ss4_uploaded`                | Signed SS-4 stored and ready to download                               |
| `signature_ss4_reminder_due`           | An unsigned SS-4 is due a reminder (day 1, 3, 7)                       |
| `signature_form8821_completed`         | A non-US founder on an expedited EIN signed their Form 8821            |
| `document_form8821_uploaded`           | Signed Form 8821 stored and ready to download                          |
| `signature_form8821_reminder_due`      | An unsigned Form 8821 is due a reminder (day 1, 3, 7)                  |
| `partner_webhook_disabled`             | Your endpoint was automatically disabled (see below)                   |

<Note>
  You may occasionally receive duplicate `document_*` events for the same document, for example after doola re-issues a document. Handle uploads idempotently using `documentId`.
</Note>

<Note>
  The three SS-4 events fire only for **non-US founders**, who must sign an SS-4 before the IRS issues the EIN. The three `form8821` events mirror them and fire only for a non-US founder who **also requested an expedited EIN**, where a signed Form 8821 authorizes doola to retrieve the EIN letter from the IRS. In each pair, `signature_*_completed` fires the instant the founder signs; `document_*_uploaded` fires once the signed PDF is stored and downloadable. See [Non-US founders](/api/non-us-formation) for the full flow.
</Note>

<Note>
  `company_ein_issued` and `document_einletter_uploaded` both signal EIN issuance, at two distinct moments: `company_ein_issued` fires when doola records the EIN on the company (fetch the company to read the `ein` field), and `document_einletter_uploaded` fires once the IRS confirmation letter is stored and downloadable. They arrive independently, so do not assume an order. `company_ein_issued` fires only on the first issuance; a later correction to the EIN value does not re-fire it.
</Note>

<Note>
  The two governance-document events are entity-type specific: `document_corporatebylaws_uploaded` fires for **CCorp** companies only, and `document_operatingagreement_uploaded` fires for **LLC** companies only. doola generates the governance document shortly after the Articles of Organization are processed, so each event typically arrives soon after `document_aoo_uploaded`.
</Note>

### Example payloads

A sample delivery for every event. Within `eventPayload`, only the fields relevant to that event are present; inapplicable fields are omitted entirely rather than sent as `null`, so parse defensively. `eventPayload` itself is `null` only for `partner_webhook_disabled`.

<AccordionGroup>
  <Accordion title="company_formation_submitted">
    ```json theme={null}
    {
      "eventId": "31pLeQk8Mcp7rSflgdr1c2ZV2th",
      "eventName": "company_formation_submitted",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ"
      },
      "timestamp": 1756216100000
    }
    ```
  </Accordion>

  <Accordion title="company_formation_completed">
    ```json theme={null}
    {
      "eventId": "31q8RtBnVGPk26B6ZqeUFeAaMBE",
      "eventName": "company_formation_completed",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ"
      },
      "timestamp": 1756392000000
    }
    ```
  </Accordion>

  <Accordion title="company_formation_failed">
    ```json theme={null}
    {
      "eventId": "31qC2fK9mNpL7vWxYzAbCdEfGhJ",
      "eventName": "company_formation_failed",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "doolaCustomerId": "31pLCw7sN2aQ9fJkRtY3vXbHmLe",
        "reasonCode": "FORMATION_SUBMISSION_FAILED",
        "message": "Doola formation submission failed after the configured retry budget. Please contact support."
      },
      "timestamp": 1756220000000
    }
    ```
  </Accordion>

  <Accordion title="company_filing_date_updated">
    ```json theme={null}
    {
      "eventId": "31qF5gH3kMqR8sT2uVwXyZaBcDe",
      "eventName": "company_filing_date_updated",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ"
      },
      "timestamp": 1756300000000
    }
    ```
  </Accordion>

  <Accordion title="company_ein_issued">
    ```json theme={null}
    {
      "eventId": "31wZ4rY6tA3fSkPqWxD8bCgMrQj",
      "eventName": "company_ein_issued",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ"
      },
      "timestamp": 1756750000000
    }
    ```
  </Accordion>

  <Accordion title="document_aoo_uploaded">
    ```json theme={null}
    {
      "eventId": "31pLdpvMh4OfO90moxLgLT8AuQr",
      "eventName": "document_aoo_uploaded",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "documentId": "31pLdj8SKAU9BYNchcohyto2AxC",
        "documentType": "ArticlesOfOrganization",
        "documentCreatedAt": "2025-08-26 13:49:16.0"
      },
      "timestamp": 1756216157000
    }
    ```
  </Accordion>

  <Accordion title="document_einletter_uploaded">
    ```json theme={null}
    {
      "eventId": "31rT8gH9kM4qR2sTuVwXyZaBcDf",
      "eventName": "document_einletter_uploaded",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "documentId": "31rTm4kP9nQ2aLfJkRtY3vXbHmL",
        "documentType": "EinLetter",
        "documentCreatedAt": "2025-09-02 08:11:42.0"
      },
      "timestamp": 1756800702000
    }
    ```
  </Accordion>

  <Accordion title="document_mail_uploaded">
    ```json theme={null}
    {
      "eventId": "31sV2kP7nQ9aLfJkRtY3vXbHmLg",
      "eventName": "document_mail_uploaded",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "documentId": "31sVq8nT6kM4qR2sTuVwXyZaBcD",
        "documentType": "Mail",
        "documentCreatedAt": "2025-09-10 17:05:30.0"
      },
      "timestamp": 1757523930000
    }
    ```
  </Accordion>

  <Accordion title="document_corporatebylaws_uploaded">
    ```json theme={null}
    {
      "eventId": "31tC3nS7qT9eMhNoPvX5zFgLrSm",
      "eventName": "document_corporatebylaws_uploaded",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "documentId": "31tC7rY8wB2fNiOpQwA7aGhMsTn",
        "documentType": "CorporateBylaws",
        "documentCreatedAt": "2025-08-26 13:52:41.0"
      },
      "timestamp": 1756216361000
    }
    ```
  </Accordion>

  <Accordion title="document_operatingagreement_uploaded">
    ```json theme={null}
    {
      "eventId": "31tC5pU9xD4gOjPqRsB8bHiNtUo",
      "eventName": "document_operatingagreement_uploaded",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "documentId": "31tC9sZ0yE6hPkQrTwC9cJjOuVp",
        "documentType": "OperatingAgreement",
        "documentCreatedAt": "2025-08-26 13:53:07.0"
      },
      "timestamp": 1756216387000
    }
    ```
  </Accordion>

  <Accordion title="signature_ss4_completed">
    ```json theme={null}
    {
      "eventId": "31tW5mR3pS7bMgKlSuZ4wYcInMf",
      "eventName": "signature_ss4_completed",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ"
      },
      "timestamp": 1756250000000
    }
    ```
  </Accordion>

  <Accordion title="document_ss4_uploaded">
    ```json theme={null}
    {
      "eventId": "31tW9nT6qU8cNhLmTvA5xZdJoNg",
      "eventName": "document_ss4_uploaded",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "documentId": "31tWp2rX5sZ7bMgKlSuZ4wYcInM",
        "documentType": "SignedSS4",
        "documentCreatedAt": "2025-08-27 10:22:09.0"
      },
      "timestamp": 1756290129000
    }
    ```
  </Accordion>

  <Accordion title="signature_ss4_reminder_due">
    ```json theme={null}
    {
      "eventId": "31uX1pV4rW9dOiMnUwB6yAeKpOh",
      "eventName": "signature_ss4_reminder_due",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "message": "SS4 signature is still pending. Request a fresh signing URL and re-present it to the user.",
        "documentType": "SS4",
        "daysSinceRequested": 1
      },
      "timestamp": 1756160000000
    }
    ```
  </Accordion>

  <Accordion title="signature_form8821_completed">
    ```json theme={null}
    {
      "eventId": "31vA2qW5sT8dPjNoVxC7zBfLqPi",
      "eventName": "signature_form8821_completed",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ"
      },
      "timestamp": 1756250050000
    }
    ```
  </Accordion>

  <Accordion title="document_form8821_uploaded">
    ```json theme={null}
    {
      "eventId": "31vB6rY8tV0eQkOpWyD8aCgMrQj",
      "eventName": "document_form8821_uploaded",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "documentId": "31vBq3sZ7tA9cNhLmTvA5xZdJoN",
        "documentType": "SignedForm8821",
        "documentCreatedAt": "2025-08-27 10:24:41.0"
      },
      "timestamp": 1756290281000
    }
    ```
  </Accordion>

  <Accordion title="signature_form8821_reminder_due">
    ```json theme={null}
    {
      "eventId": "31vC4sZ7uX1fRlPqXzE9bDhNsRk",
      "eventName": "signature_form8821_reminder_due",
      "eventPayload": {
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "message": "Form 8821 signature is still pending. Request a fresh signing URL and re-present it to the user.",
        "documentType": "FORM8821",
        "daysSinceRequested": 1
      },
      "timestamp": 1756160050000
    }
    ```
  </Accordion>

  <Accordion title="partner_webhook_disabled">
    ```json theme={null}
    {
      "eventId": "31vY3qX5sZ2eRjNoVxC7zBfLqPi",
      "eventName": "partner_webhook_disabled",
      "eventPayload": null,
      "timestamp": 1756600000000
    }
    ```
  </Accordion>
</AccordionGroup>

#### Payload fields

`doolaCompanyId` is on every company- and document-scoped payload. The rest appear only on the events noted below:

| Field                | Appears in                                                                                  | Meaning                                                                                                                                                                                    |
| -------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `documentId`         | `document_*`                                                                                | ID of the uploaded document. Use it with the [document endpoints](/api/domain-objects#document) to download.                                                                               |
| `documentType`       | `document_*`, `signature_ss4_reminder_due`, `signature_form8821_reminder_due`               | `ArticlesOfOrganization`, `EinLetter`, `Mail`, `CorporateBylaws`, `OperatingAgreement`, `SignedSS4`, or `SignedForm8821` on document events; `SS4` or `FORM8821` on the matching reminder. |
| `documentCreatedAt`  | `document_*`                                                                                | When doola stored the document.                                                                                                                                                            |
| `doolaCustomerId`    | `company_formation_failed`                                                                  | The customer who owns the company. Best-effort; omitted if unavailable.                                                                                                                    |
| `reasonCode`         | `company_formation_failed`                                                                  | Machine-readable failure reason. Branch on this, never on `message`.                                                                                                                       |
| `message`            | `company_formation_failed`, `signature_ss4_reminder_due`, `signature_form8821_reminder_due` | Human-readable context for the event.                                                                                                                                                      |
| `daysSinceRequested` | `signature_ss4_reminder_due`, `signature_form8821_reminder_due`                             | Days since that document was first requested: `1`, `3`, or `7`.                                                                                                                            |

## Delivery and retries

If your endpoint does not return a `2xx`, doola retries up to 5 times: at 1 minute, 15 minutes, 1 hour, 12 hours, and 24 hours after the previous attempt. Treat every delivery as at least once.

After all retries fail, doola automatically disables your endpoint and sends a final `partner_webhook_disabled` event. Fix your endpoint and re-enable it in the Partner Portal. Re-enabling is a deliberate partner action, so you confirm the endpoint is healthy before traffic resumes.

Every delivery attempt (timestamp, event, HTTP status, attempt number) is visible in the Partner Portal.
