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

Setup

1

Generate your signing secret

In the doola Partner Portal, open Settings → Webhook URL and generate your signing secret. It is shown once — copy it into your secret manager before you leave the page. Generating requires an active API key on the tenant, so mint one under Settings → API Keys first.Rotating is the same button and takes effect immediately: the previous secret stops signing the moment the new one is issued, and deliveries fail your verification until you have stored the new value. Rotate when you can deploy the new secret, not mid-incident.
2

Configure your endpoint

Set your webhook URL in the same Settings → Webhook URL tab. The URL must be HTTPS. Each environment has its own tenant, so configure sandbox and production separately — each has its own signing secret.
To see real deliveries end to end without waiting on an actual formation, use the Sandbox playground: it completes the formation and EIN milestones on demand and fires the events below through the normal pipeline.

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.) The value is the bare hex digest, with no sha256= prefix, and the body is the only thing signed: there is no timestamp header. Until you generate a signing secret, deliveries carry no X-Doola-Signature header at all. Generate the secret before you go live, and reject any request whose signature is missing or does not match.
Always use a constant time comparison (timingSafeEqual or compare_digest). Plain string equality is vulnerable to timing attacks.

Payload

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

Events

A document_* event carries the documentType, not the document’s ID. On each one, list the company’s documents and process any you have not seen, keyed on the document id. The same document_* event can fire more than once for a company, with a new eventId each time, for example when doola re-issues a document, so the document id is your dedupe key, not the event.
SS-4 events apply to companies that require an SS-4 under doola’s workflow, including ITIN-only companies. The three form8821 signing milestones — completed, uploaded, and reminder — mirror the SS-4 ones and fire for whoever is asked to sign a Form 8821: a founder whose company requires SS-4 and also requested expedited EIN, where the signed form authorizes doola to retrieve the EIN letter from the IRS, or any founder doola has asked through a Form 8821 request. In each pair, signature_*_completed fires once doola has recorded the signature, normally seconds after the founder signs; document_*_uploaded fires once the signed PDF is downloadable. signature_ss4_reset fires only when a founder had already signed and a later name change voided that signature. These events do not determine residency. doola raises signature_form8821_required when it could not obtain the EIN online. See SS-4 and Form 8821 signing for the full flow.
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.
company_name_options_required, signature_ss4_reset, signature_form8821_required and company_founder_address_required announce a required action: something doola needs from you before that company can continue. They are the only events that block progress until you act. All four carry a requiredActionId: key your records on it and read the action against it. company_name_options_required and company_founder_address_required are answered through the resolution endpoint; the two signature actions close themselves once the signature completes. An action you never received, because your endpoint was down, stays open and is readable from the open list.
The annual report events follow a report you filed through Annual Report Filing. company_annual_report_submitted fires once you confirm the report and doola starts filing it. company_annual_report_completed fires once the state has the report, and is terminal: it never fires twice for the same report. Both carry doolaCompanyId only, so list the company’s annual reports to see which year moved. document_annualreport_uploaded fires separately, once the filed report is stored and downloadable. It arrives independently of the other two, so do not assume an order; a filing may complete before any document exists.
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.

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.
Not currently sent: no webhook fires when doola corrects a filing date. The payload below is the shape it will have if it is enabled.

Payload fields

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

Delivery and retries

A delivery succeeds when your endpoint answers 2xx within 60 seconds. A 4xx, a 5xx, a timeout or a connection error is retried up to 5 times: at 1 minute, 15 minutes, 1 hour, 12 hours, and 24 hours after the previous attempt, about 37 hours in all. Treat every delivery as at least once. Redirects are not followed. A 3xx answer fails that delivery and is not retried, so point the webhook URL at the final host. When an event exhausts its retries, doola disables your endpoint. No notification is sent, and events that fire while the endpoint is disabled are not delivered or queued. Watch the Events page, fix your endpoint, re-enable it in the Partner Portal, then run a reconciliation sweep to pick up what you missed. Re-enabling is a deliberate partner action, so you confirm the endpoint is healthy before traffic resumes. GET /v1/partner/webhook/events and the Partner Portal’s Settings → Webhook Events tab still list a partner_webhook_disabled event. It is never delivered, so don’t wait for it. Every delivery attempt (timestamp, event, HTTP status) is visible on the Partner Portal’s Events page, for the last 90 days. eventId is stable across every retry of the same event, so dedupe on it and keep your handler idempotent.

Reconciliation

Webhooks are how you learn about a change quickly. They are not the only way to learn about it: every state a webhook announces is also readable from the API, so a missed delivery — your downtime, a disabled endpoint, a dropped retry — is always recoverable without contacting doola. Read the company:
A reconciliation pass that covers everything above:
  1. Select the companies you believe are still in flight — anything whose Formation or EinCreation service is not yet Completed.
  2. Fetch each one and apply the table above, treating each field as the source of truth over your cached copy.
  3. Call GET /v1/partner/required-actions once. Anything open there that you have no record of is blocking a formation and needs your input.
Run it on startup, on a schedule, and after any incident on your side or after re-enabling a disabled endpoint. Hourly is plenty: formation milestones are days apart, so a slow sweep loses nothing.
A reconciliation sweep is a backstop, not a substitute for webhooks. Polling every company on a tight interval earns you a 429 E_RATE_LIMITED long before it makes your data fresher — the events already tell you the moment something changes.
Last modified on October 23, 2026