---
name: doola
description: Use when integrating the doola Partner API to form US LLCs or C-Corps for your customers, track formations to completion with webhooks, resolve required actions, collect SS-4 and Form 8821 signatures, or file annual reports. Also covers the doola Embedded SDK (@doola/js), which embeds doola's formation flow in a partner's product while the partner takes payment, and the doola Formation MCP server, which lets a founder form a Wyoming LLC from their own AI client with their own doola account.
compatibility: The Partner API needs a doola partner API key (dk_test_ for sandbox, dk_live_ for production). The Embedded SDK also needs a publishable key (pk_test_ or pk_live_). The Formation MCP server needs OAuth sign-in with the founder's own doola account and has no sandbox.
metadata:
  author: doola
  version: "2.1"
---

# doola

doola forms US companies and keeps them compliant. There are three ways in, and they serve different people. Pick one before writing any code.

| | Partner API | Embedded SDK | Formation MCP server |
| --- | --- | --- | --- |
| Who uses it | A partner's backend, forming companies for the partner's customers through the partner's own screens | A partner that embeds doola's formation screens in its product and takes payment itself | A founder, forming their own company from their own AI client |
| Auth | API key in `Authorization` | Publishable key in the browser; the API key on the partner's server mints short-lived customer sessions | OAuth, signed in as the founder's own doola account |
| Entities | LLC or C-Corp, any state | LLC or C-Corp, any state | Wyoming LLC on the Starter package only |
| Test environment | Sandbox at `https://api.test.doola.com` | `pk_test_` and `dk_test_` keys, same sandbox | None. Every run charges a real card and files a real company |
| Progress | Webhooks, plus reads for reconciliation | `onFormed` in the browser, then the Partner API's webhooks | Ask `get_company_info`, or send the founder to their doola dashboard |

The MCP server is not a way for a partner to form companies for its customers. It takes no API keys and acts only on the signed-in founder's account. Partners integrate the Partner API or the Embedded SDK.

## Partner API

### Basics

| Environment | Base URL | Key prefix |
| --- | --- | --- |
| Sandbox | `https://api.test.doola.com` | `dk_test_` |
| Production | `https://api.doola.com` | `dk_live_` |

- **Auth.** Send the raw key: `Authorization: dk_test_...`. No `Bearer` prefix. A key only works in its own environment. doola issues keys; request access at https://www.doola.com/partner-with-us/.
- **Envelope.** Every response is `{"payload": ..., "error": null}` on success and `{"payload": null, "error": {...}}` on failure. Read `doolaCustomerId` and friends from `payload`, never the top level. Only three lists are paged, under `payload.content` with `page`, `size`, `total` and `totalPages`: `GET /v1/partner/customers`, `GET /v1/partner/companies` and `GET /v1/partner/required-actions`. Every other list (reference data, and a company's documents, required actions and annual reports) returns `payload` as a plain array.
- **Errors.** Branch on `error.code`, never on `message`. Validation failures add per-field codes under `error.fields`. Log `error.requestId` (the same value as the `x-request-id` response header) and quote it to doola support.
- **Idempotency.** `POST /v1/partner/customers` and `POST /v1/partner/companies` require an `Idempotency-Key` header (missing: `400 E_IDEMPOTENCY_KEY_REQUIRED`). Use one key per logical create, tied to your own record. Retrying with the same key and body is safe. A failed create releases the key, so fix the request and retry with it. After a success, the same key with a different body returns `409 E_IDEMPOTENCY_KEY_REUSED`; a concurrent request on the same key returns `409 E_IDEMPOTENCY_IN_PROGRESS`. No other endpoint takes the header.
- **Rate limit.** Requests are rate-limited per tenant. On `429 E_RATE_LIMITED`, wait the `Retry-After` seconds before retrying.

### Form a company

1. **Set up webhooks first.** In the Partner Portal, open **Settings → Webhook URL**, generate the signing secret (it needs an active API key on the tenant, and is shown once) and set an HTTPS URL. Sandbox and production are separate tenants with separate secrets. Events raised while no URL is set are not delivered later, and `company_formation_submitted` fires shortly after the create.
2. **Create the customer.** `POST /v1/partner/customers` with `email`, `firstName`, `lastName`, `countryOfResidence` (ISO 3166-1 alpha-3, e.g. `USA`) and optionally `phoneNumber` (with the international code). Save `payload.doolaCustomerId`. An email that is already your customer returns `200` with `created: false`. An email that belongs to anyone else returns `409 E_EMAIL_IN_USE`. One customer can own many companies.
3. **Create the company.** `POST /v1/partner/companies` with the `doolaCustomerId`. This starts the formation, and **the formation charge applies at this request**, even if every name is later rejected. Never create test companies in production.
4. **Read `signatureRequirements`** on the create or get-company response, and collect any signatures it lists (see Signatures).
5. **Follow progress over webhooks,** not a polling loop (see Tracking).
6. **Fetch documents** as their `document_*` events arrive.

Create-company rules that get requests rejected:

- `entityType` is `LLC` (the default) or `CCorp`.
- **LLC:** send `members`, with ownership totalling exactly 100. Omit `executiveMembers` and `ccorpValuation`.
- **CCorp:** send `executiveMembers` and `ccorpValuation`, and omit `members`. The executives must include at least one President, Secretary, Treasurer and Director. President, Secretary and Treasurer may each appear only once; Director may repeat.
- `nameOptions`: 1 to 3, first choice first. On create the array order sets the preference and any `position` you send is ignored; responses return the stored order starting at 0. A name-options resolution does honour `position`. Send `name` without a suffix and put the suffix in `entityTypeEnding`, which is case-sensitive and must match the entity type. LLC: `LLC`, `L.L.C`, `L.L.C.`, `Limited Liability Company`. CCorp: `Inc`, `Inc.`, `Incorporated`, `Corp.`, `Corporation`, `C Corp`, `C Corporation`, `CCorp`, `Company`. Submit all three options; it lowers the chance of a name conflict.
- `addresses`: exactly two, one `mailing` and one `business`. Each has a `provider`; with the `registeredAgent` provider, omit `address`. In `IN` and `IL` send both with `provider: customer` and the founder's own US address: those states reject the registered agent's address, and a create that uses it is held behind a required action.
- `state`: a two-letter code. `industry`: an exact label from the NAICS reference data. `description`: 50 characters or fewer. `responsibleParty` is required.
- Expedited EIN (an extra fee per use): add `requestedServices: [{"service": "EinCreation", "variant": "Expedite"}]` on the create. It is available only when no one on the company supplies a valid-format SSN; otherwise the create returns `400 E_EXPEDITED_EIN_NOT_ELIGIBLE`.

### Signatures (SS-4 and Form 8821)

- An SS-4 is required when no member, executive member or responsible party supplies an SSN in a valid format. An ITIN never counts as that SSN. This is a format check, not a residency check.
- A Form 8821 is also required at formation when the company requested Expedited EIN. doola can ask for one later through a required action.
- To collect a signature, call `POST /v1/partner/companies/{companyId}/signatures` with `documentType` `SS4` or `FORM8821`, right before you show the link. Links expire two hours after issue, and a new session voids the previous one, so never cache or email a link.
- **doola does not remind the founder.** On `signature_ss4_reminder_due` or `signature_form8821_reminder_due` (days 1, 3 and 7), mint a fresh session and nudge the founder yourself.

### Tracking

A formation has two signals, and confusing them is the most common integration mistake.

| Question | Read | Webhook |
| --- | --- | --- |
| Where is my request? | `formationSubmissionStatus`: `PENDING`, then `SUBMITTED`, then `COMPLETED` once the state filing is recorded, or `FAILED` | `company_formation_submitted` / `company_formation_failed` |
| Is the company formed with the state? | The `Formation` entry in `services` reaching `status: "Completed"` | `company_formation_completed` |
| Is the EIN issued? | `ein` populated on the company | `company_ein_issued` |
| When was it filed? | `formationFilingDate`, `formationFilingNumber` | `company_formation_completed` (first recorded; a later correction sends no webhook) |

- Formation is complete when `company_formation_completed` fires; from then on `formationSubmissionStatus` reads `COMPLETED` and the `Formation` service reads `Completed`. For the EIN and the registered agent, read `services`. `AWAITING_PAYMENT` appears only on companies submitted through the doola SDK, and so does `CANCELLED`: an unpaid SDK formation doola withdrew at your request.
- EIN events arrive on the IRS's timeline, independently of the formation events. Don't assume an order.
- `FAILED`, or a company stuck in `PENDING`: read `adminNotes` on the company, then contact doola.
- Documents: a `document_*` event carries `documentType`, not the document's ID. On each one, list `GET /v1/partner/companies/{companyId}/documents`, then call `GET /v1/partner/companies/{companyId}/documents/{documentId}` for any document you have not stored. Its `downloadUrl` is a signed URL valid for about an hour, so fetch it when you need it. The same event can fire more than once for a company; dedupe on the document `id`.

### Webhooks

- Verify `X-Doola-Signature`: a hex HMAC-SHA256 of the **raw** request body, keyed with your signing secret. Compute it over the raw bytes before parsing JSON, and compare in constant time.
- Delivery is at least once. `eventId` is stable across retries of the same event; dedupe on it and keep handlers idempotent.
- A `4xx`, `5xx`, connection error or no answer within 60 seconds is retried 5 times (1 min, 15 min, 1 h, 12 h, 24 h). A `3xx` is not followed or retried. When an event exhausts its retries doola disables the endpoint, with no notification. Fix it, re-enable it in the Partner Portal, then reconcile. Events raised while it was disabled are not replayed.
- The `X-Doola-Signature` header is only sent once a signing secret exists. Reject unsigned requests.
- Rotating the secret takes effect immediately. Rotate only when you can deploy the new value.

### Required actions

A required action is doola needing something from you before a company can continue. Only four events announce one, and they are the only events that block progress: `company_name_options_required`, `signature_ss4_reset`, `signature_form8821_required` and `company_founder_address_required`. Each carries a `requiredActionId`; key your records on it.

- **New names needed** (`actionCode` `FORMATION_NAME_OPTIONS_EXHAUSTED`): the state rejected every name. For an SDK company the founder can send new names once from the SDK dashboard, and a partner submission replaces them. Otherwise collect new names from your user and call `POST /v1/partner/companies/{companyId}/required-actions/{requiredActionId}/resolution` with `{"actionCode": "FORMATION_NAME_OPTIONS_EXHAUSTED", "nameOptions": [...]}`. This endpoint takes no `Idempotency-Key` and is **not** idempotent: two identical calls apply the names twice. After a timeout, read the action back instead of retrying.
- **Founder address needed** (`actionCode` `FORMATION_FOUNDER_ADDRESS_REQUIRED`): an `IN` or `IL` company was created with the registered agent's address, and its formation waits at `PENDING`. Call the same resolution endpoint with `{"actionCode": "FORMATION_FOUNDER_ADDRESS_REQUIRED", "addresses": [...]}`: one `business` and one `mailing` entry, both `provider: customer` with a US address. The action closes as `resolved` at once and the formation continues; a retry after that returns `409 E_REQUIRED_ACTION_CLOSED`, so read the action back after a timeout.
- **Signature actions** close themselves once the founder signs. Mint a signing session for the document; don't post a resolution (that returns `422 E_REQUIRED_ACTION_NOT_RESOLVABLE`).
- **Open list:** `GET /v1/partner/required-actions?size=100` returns every open action across your companies. `size` caps at 100; page while `total` exceeds it.

### Reconciliation

Every state a webhook announces is also readable from the API, so a missed delivery is always recoverable. Don't poll in a loop. Instead, sweep on startup, on a schedule (hourly is plenty) and after any outage or disabled endpoint:

1. Read the open required-actions list.
2. Re-read each in-flight company with `GET /v1/partner/companies/{companyId}` and compare it with your records, using the Tracking table above.

### Compliance

- **Calendar:** `GET /v1/partner/companies/{companyId}/compliance/calendar` lists a company's tracked obligations and due dates.
- **Annual report filing** takes two writes, and nothing is filed until the second:
  1. `GET .../compliance/annual-reports/requirements` for the fields to collect (most states ask none) and `GET .../compliance/annual-reports/state-fees` for what the state charges.
  2. `POST .../compliance/annual-reports` records the report as `FILLED` and fixes its price. It does not file. Submitting again for the same year while it is still `FILLED` replaces it (`isReplaced: true`).
  3. `POST .../compliance/annual-reports/{annualReportId}/confirm` hands it to doola to file and fires `company_annual_report_submitted`. Confirm every report, including one priced at `0`. Confirming twice is safe.
  4. `company_annual_report_completed` fires once the state has it; `document_annualreport_uploaded` fires separately when the filed report is downloadable. Both report events carry only `doolaCompanyId`, so list the company's reports to see which year moved.

### Sandbox

- Sandbox keys never trigger real state filings or charges. Build and test there first.
- The sandbox playground completes milestones on demand and fires the real webhooks: `POST /v1/partner/playground/companies/{companyId}/formation/complete` and `POST /v1/partner/playground/companies/{companyId}/eincreation/complete`.
- The playground cannot raise required actions. To test one, ask engineering@doola.com to raise it on a sandbox company.

### Before you ship

- [ ] Webhook URL and signing secret configured in each environment before the first create
- [ ] Signature verified over the raw body with a constant-time compare
- [ ] Handlers dedupe on `eventId`, and on the document `id` from the documents list for `document_*` events
- [ ] `Idempotency-Key` on both creates, stable across retries of the same logical create
- [ ] "Formed" comes from `company_formation_completed`, and reconciliation checks `formationSubmissionStatus: COMPLETED` or the `Formation` service
- [ ] Name resolution is sent once, never retried blindly
- [ ] You send the SS-4 and Form 8821 reminders
- [ ] Reconciliation sweep on startup and on a schedule; no polling loops
- [ ] Errors handled by `error.code`, and `429` honours `Retry-After`
- [ ] Every annual report confirmed after it is submitted

## Embedded SDK

A partner embeds doola's formation wizard (an iframe served from `sdk.doola.com`) with `@doola/js`. The founder's details, tax ids and signatures go from the iframe straight to doola. The partner owns sign-in and checkout. Full docs: https://docs.doola.com/sdk/overview

### Keys and hosts

| | Test | Live |
| --- | --- | --- |
| Publishable key (browser) | `pk_test_` | `pk_live_` |
| Secret key (server only) | `dk_test_` | `dk_live_` |
| Embedded app | `https://sdk.test.doola.com` | `https://sdk.doola.com` |
| Partner API | `https://api.test.doola.com` | `https://api.doola.com` |

The publishable key is in the Partner Portal under SDK, then Install (or `GET /v1/partner/sdk/keys` with the secret key). The loader always loads from `https://js.doola.com/v1/doola.js`.

### Integrate

1. **Session route on the partner's server.** `POST /v1/partner/customer-sessions` with the raw `dk_` key and `{ email, externalCustomerId? }` for the signed-in user, taken from the partner's own auth, never the request body. Send only an email the partner has verified, and always send `externalCustomerId`. Return only `{ accessToken, expiresIn }` from `payload`, with `Cache-Control: no-store`. Answer 401 only when the user is signed out, and turn doola's 401 into a 502. Pass other statuses through, and send a 409 with doola's `error.code` as `{ code }`. `@doola/js/server` (`createSessionHandler`, `createCustomerSession`) does all of this. Sessions last 600 seconds; the loader renews them.
2. **Mount in the browser.** `loadDoola({ publishableKey, fetchAccessToken, onAuthError, onFormed })`, then append `doola.create()`. `fetchAccessToken` rejects with `{ status, code }` on failure, `code` read from the route's JSON body. Options are fixed by the first `loadDoola` call on the page; a second call with the same key returns the live instance and ignores new options, so keep one module-level instance and route callbacks through it. Call `destroy()` on sign-out only.
3. **Take payment.** `onFormed({ companyId })` fires on submit and again whenever the founder asks to pay, so it must be idempotent. On the server: `GET /v1/partner/companies/{companyId}`, charge only while `formationSubmissionStatus` is `AWAITING_PAYMENT` (`PENDING`/`SUBMITTED`/`COMPLETED` are paid; refuse `CANCELLED`, `FAILED` and unknown statuses); `GET /v1/partner/customers/{doolaCustomerId}` and compare `externalCustomerId` with the signed-in user's id, or `email` only when it is null; price from the company's `entityType` and `state` plus `GET /v1/partner/references/state-fees?entityType=`; keep one order per `companyId` and charge with an idempotency key per attempt (`${companyId}:${attemptId}`), since providers replay a declined result for a reused key; then `POST /v1/partner/companies/{companyId}/payment-confirmed` with optional `{ partnerReference }`. Never trust a price or status from the browser.
4. **Replace the element** after confirming: `element.replaceWith(doola.create())`. The payment screen in the iframe never polls, so only a new element shows the founder's company. Each `create()` is a separate iframe, so never append a second one beside the old.

### Payment confirmation

- `200` with an **empty body** (not the envelope): confirmed, or already confirmed (no-op). Also a no-op for companies created through the Partner API.
- `404 E_NOT_FOUND`: not your company. `409 E_COMPANY_CANCELLED`: doola cancelled it before payment; refund the founder.
- No webhook fires while a company is `AWAITING_PAYMENT`; the first is `company_formation_submitted` after confirmation. Save `companyId` with the order when checkout opens, and sweep charged-but-unconfirmed orders.
- Only doola can cancel an unpaid formation. Cancelling frees the customer to start again.

### Rules

- One company per customer. A customer with a company in the tenant sees its dashboard instead of the wizard, or a closed screen for a failed one. Once doola cancels an unpaid formation, the customer can start again.
- Session mint errors: `409 E_EMAIL_IN_USE` (email has its own doola account, or belongs to a Partner Portal user), `409 E_RESOURCE_CONFLICT` (the email's customer is bound to another `externalCustomerId`, the `externalCustomerId` belongs to another customer, or an older customer cannot take a new email), `409 E_CUSTOMER_REVOKED`, `429 E_RATE_LIMITED` (600 per minute per tenant). With the code forwarded, the loader reports them as `email_in_use`, `external_id_conflict` and `customer_revoked`; without it, a 409 on the first session is `email_in_use` and on a renewal `renewal_failed`.
- In sandbox, confirm payment and wait for `company_formation_submitted` before calling the playground. It refuses an unpaid company (`422 E_FORMATION_AWAITING_PAYMENT`) and a cancelled one (`409 E_COMPANY_CANCELLED`).
- Embedded signing renders only on domains doola has allowed; partners send doola every domain that hosts the SDK.
- CSP: `script-src https://js.doola.com`, `frame-src https://sdk.doola.com` (plus `https://sdk.test.doola.com` with test keys), and `trusted-types doola-js` when Trusted Types are enforced.

## Formation MCP server

For a founder who wants to form a US LLC by talking to their own AI client. The server drives the flow; the agent follows it one step at a time.

- **Endpoint:** `https://mcp.doola.com` (Streamable HTTP). Clients sign in with OAuth 2.0 (authorization code with PKCE). The founder signs in with their own doola account, or signs up during OAuth. Setup for Claude, ChatGPT, Perplexity, Cursor and other clients is at https://docs.doola.com/mcp/connect.
- **Scope:** Wyoming LLC on the Starter package only, enforced by the server. No other states, no C-Corp or S-Corp, no dissolution, no post-formation compliance.
- **No test mode.** Payment is real and the filing is real.

### Tools

| Tool | Use |
| --- | --- |
| `get_company_onboarding_status` | Once at the start of a session to find the next step. Call it again only to resume, recover from an error, or after switching company |
| `start_formation` | Legal name (only when the profile lacks it), country of residence and company name; sets Wyoming |
| `create_checkout_session` | Accepts the Terms of Service and returns `pricing` with the Stripe checkout |
| `verify_payment` | Confirms the checkout was paid |
| `search_industries` | The industry list to match the founder's business against |
| `save_company_details` | Every post-payment detail: industry, backup names, addresses, members with ownership, responsible party |
| `submit_formation` | Files the formation |
| `list_companies` | Every company the founder owns, with `isActive` |
| `start_new_company` | Begin another formation alongside existing ones, only when the founder asks |
| `switch_active_company` | Point the tools at a different company |
| `get_company_info` | The full profile: status, filing date, EIN, members, services |

Flow order: `start_formation`, `create_checkout_session`, `verify_payment`, then after payment `search_industries` and `save_company_details` for each answer, and `submit_formation` last. `save_company_details` saves whatever sections it is given, so save each answer as the founder gives it.

### Driving the flow

- Follow `nextAction` verbatim. Don't plan the flow yourself.
- Treat `nextActionInput` as a template, not a payload to echo. It is `{tool, arguments}`: call that tool and replace every `<placeholder>` in `arguments` with the founder's real answer.
- Pass objects and arrays such as `members` as native JSON, never as strings.
- On `error: true`, fix the input as `message` describes and retry, or follow the `nextAction` it returns. If there is none, call `get_company_onboarding_status`.
- Explain briefly why each piece of information is needed.

### Rules that protect the founder

- **Never invent or suggest company names.** They come from the founder. Sending `companyName` to `start_formation` again replaces the name and wipes the backups.
- **`create_checkout_session` accepts doola's Terms of Service,** so present the Starter package first. Then quote `pricing.summary` (the first-year list price) and `chargeSummary` (what Stripe charges today) exactly as returned. The list price includes the recurring Starter subscription, so never call it a one-time fee or today's charge, and never hardcode or add up prices yourself.
- **Payment:** `create_checkout_session` also returns a Stripe `checkoutUrl`; give it to the founder unchanged, since card details never pass through the agent. Once they say they've paid, call `verify_payment` with the `checkoutSessionId`. Ask no post-payment question until it reports the payment complete.
- `industry` must be an exact entry from `search_industries`, and `description` is 50 characters or fewer. Longer values are rejected, not truncated.
- The registered address must be in Wyoming and the mailing address in the US.
- `members` replaces the saved members as a whole, so always send every member with all of their fields. Ownership percentages must total exactly 100.
- Confirm the responsible party's email with the founder. SS-4 and Form 8821 are sent there for signing.
- With several companies, check which one `list_companies` marks `isActive` before writing.

### Submitting

1. Show a complete summary table of everything collected.
2. Wait for the founder to reply "submit" explicitly.
3. Call `submit_formation` with `{"userConfirmed": true}`.

This is **irreversible**. It submits the formation to doola for filing with the Wyoming Secretary of State and emails IRS Form SS-4 and Form 8821 to the responsible party to sign.

- `partialFailures` in the result means the filing went through but an email did not. Tell the founder the formation succeeded and to contact doola support to resend the forms.
- A `FILING_FAILED` error means the filing request did not go through. Tell the founder to contact doola support, and don't keep retrying `submit_formation`.
- After submitting there is no push channel. Share the returned `dashboardUrl`, and use `get_company_info` when the founder asks where things stand.

## Resources

- Full page index: https://docs.doola.com/llms.txt
- Partner API: [introduction](https://docs.doola.com/api/introduction), [authentication](https://docs.doola.com/api/authentication), [formation](https://docs.doola.com/api/formation), [SS-4 and Form 8821 signing](https://docs.doola.com/api/non-us-formation), [webhooks](https://docs.doola.com/api/webhooks), [required actions](https://docs.doola.com/api/required-actions), [annual report filing](https://docs.doola.com/api/compliance/annual-report-filing), [sandbox playground](https://docs.doola.com/api/sandbox-playground), [errors](https://docs.doola.com/api/errors), [domain objects](https://docs.doola.com/api/domain-objects)
- Embedded SDK: [overview](https://docs.doola.com/sdk/overview), [quickstart](https://docs.doola.com/sdk/quickstart), [sessions](https://docs.doola.com/sdk/sessions), [embed](https://docs.doola.com/sdk/embed), [take payment](https://docs.doola.com/sdk/payments), [errors](https://docs.doola.com/sdk/reference/errors), [go-live checklist](https://docs.doola.com/sdk/go-live)
- Formation MCP server: [introduction](https://docs.doola.com/mcp/introduction), [connect a client](https://docs.doola.com/mcp/connect), [tools](https://docs.doola.com/mcp/tools), [end-to-end flow](https://docs.doola.com/mcp/flow), [limitations](https://docs.doola.com/mcp/limitations)
