> ## 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.

> ## Agent Instructions
> Authenticate with the raw API key in the Authorization header. Never add a Bearer prefix.
> Use the sandbox base URL https://api.test.doola.com and a dk_test_ key in examples unless the user asks for production.
> Send an Idempotency-Key header on POST /v1/partner/customers and POST /v1/partner/companies. No other endpoint takes one.
> Every response is wrapped as {payload, error}. Read fields from payload and branch on error.code, never on the message.
> Drive formation progress from webhooks plus a reconciliation sweep, never a polling loop.
> Embedded SDK (@doola/js): the dk_ key stays on the server and mints customer sessions; the browser gets only a pk_ key and the session. onFormed carries only companyId and can fire more than once. Before charging, read the company with the dk_ key, require formationSubmissionStatus AWAITING_PAYMENT, check the owner's email, and price on the server. Then POST payment-confirmed, which answers 200 with an empty body, and replace the SDK element with a new doola.create() (never append a second one).
> The doola Formation MCP server at https://mcp.doola.com is for founders forming their own company with their own doola account. Partners integrate the Partner API or the Embedded SDK. The /mcp endpoint on this docs site only searches these docs.
> For a full capability summary with the rules above, read https://docs.doola.com/skill.md.

# Required actions

> Respond when doola needs something from you before a company's formation can continue.

Most of a formation runs without you. Occasionally it cannot: the state rejects every company name you submitted, a document has to be signed again, or doola needs a signature it did not ask for at formation. When that happens doola raises a **required action** on the company, tells you over a webhook, and waits.

A required action is the one thing in the Partner API that blocks on you. Until it is resolved, that company does not progress — its formation, or the EIN that follows it — so treat these events as work items, not notifications.

This page covers what every required action has in common: how you are told, how to read them, and how to recover the ones you missed. Each kind has its own page for the part that differs — how you actually clear it.

<CardGroup cols={2}>
  <Card title="New company names needed" icon="pen-to-square" href="/api/required-actions/name-options">
    `FORMATION_NAME_OPTIONS_EXHAUSTED` — the state rejected every name you submitted. You send replacements.
  </Card>

  <Card title="SS-4 signature needed again" icon="file-signature" href="/api/required-actions/ss4-reset">
    `FORMATION_SIGNATURE_SS4_RESET` — a name change voided a signed SS-4. Your founder signs again.
  </Card>

  <Card title="Form 8821 signature needed" icon="file-contract" href="/api/required-actions/form8821-required">
    `FORMATION_SIGNATURE_FORM8821_REQUIRED` — doola could not obtain the EIN online. Your founder signs a Form 8821.
  </Card>
</CardGroup>

<Note>
  A closed action is never reopened. If the same thing is needed again, doola raises a **new** action with a fresh `requiredActionId`. Key your records on `requiredActionId`, not on the company plus the code.
</Note>

## Anatomy

Every action carries a `requiredActionId`, an `actionCode` that says what is needed, and a `reason` written for a person to read. These are the three you build against:

| Field | Notes |
| - | - |
| `requiredActionId` | Identifier for the action. Resolve and read against it |
| `actionCode` | `FORMATION_NAME_OPTIONS_EXHAUSTED`, `FORMATION_SIGNATURE_SS4_RESET` or `FORMATION_SIGNATURE_FORM8821_REQUIRED`. Branch on this |
| `reason` | Explanation to render to your user verbatim. Never parse it |
| `status`, `open` and `awaitingResponse` | Where the action has got to; see [Statuses](#statuses) |

Full field list: [Required action](/api/domain-objects#required-action).

For name rejections the `reason` usually names the exact names the state turned down. When doola could not capture them it falls back to a fixed sentence, `All submitted company name options were rejected by the state. Submit new options.`

## Statuses

An action's `status` is the last thing that happened to it. `open` tells you whether it still needs attention.

| Status | Set by | Meaning |
| - | - | - |
| `created` | doola | The action was raised |
| `delivered` | doola | The webhook reached your endpoint. The action is still open and still needs your answer |
| `delivery_failed` | doola | doola could not reach your endpoint after all retries — you will only find this action by listing |
| `submitted` | you | Your resolution was received and is being processed |
| `resolved` | doola | The resolution was accepted; the action is closed |
| `rejected` | doola | The resolution was not accepted, for example the state rejected the new names too; the action is closed |

`resolved` and `rejected` are terminal, and `open` is then `false`. Every other status means the action is still open.

Once a resolution has been submitted, a later delivery outcome (`delivered` or `delivery_failed`, from a webhook retry) is still recorded in the history but no longer changes `status`.

`awaitingResponse` is `true` while the action is waiting for a resolution: `created`, `delivered` or `delivery_failed`. It turns `false` once a resolution is submitted and stays `false` until the action closes. For a company formed through the embedded SDK, the founder can submit new names from the app, so `awaitingResponse` can turn `false` without you submitting. You can still submit your own resolution after that; it replaces the names the founder sent.

<Warning>
  `submitted` is not done. It means doola has your answer and is acting on it; for name options that means [refiling with the state](/api/required-actions/name-options#the-flow). Wait for the action to close before telling your user the matter is settled.
</Warning>

## How doola tells you

Each code has its own webhook event, and all of them carry the same payload shape.

| Event | Raised for |
| - | - |
| `company_name_options_required` | [`FORMATION_NAME_OPTIONS_EXHAUSTED`](/api/required-actions/name-options) |
| `signature_ss4_reset` | [`FORMATION_SIGNATURE_SS4_RESET`](/api/required-actions/ss4-reset) |
| `signature_form8821_required` | [`FORMATION_SIGNATURE_FORM8821_REQUIRED`](/api/required-actions/form8821-required) |

```json theme={null}
{
  "eventId": "31pLdpvMh4OfO90moxLgLT8AuQr",
  "eventName": "company_name_options_required",
  "eventPayload": {
    "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
    "requiredActionId": "31wR7kM2nP5qT8vXyZaBcDeFgHj",
    "actionCode": "FORMATION_NAME_OPTIONS_EXHAUSTED",
    "actionName": "New company names needed",
    "reason": "The state rejected “Acme Holdings LLC” and “Acme Labs LLC”. Submit new company name options to continue the formation."
  },
  "timestamp": 1756216157000
}
```

Delivery follows the standard [webhook contract](/api/webhooks#delivery-and-retries), so treat it as at least once and dedupe on `eventId`. An action doola never managed to deliver is still open and still blocking — see [Reconciling](#reconciling).

## Reading actions

### Everything still open

One call, across every company you own. This is what a dashboard or a startup check should read.

```bash theme={null}
curl "https://api.test.doola.com/v1/partner/required-actions?page=0&size=20" \
  -H "Authorization: dk_test_your_api_key_here"
```

```json theme={null}
{
  "payload": {
    "content": [
      {
        "requiredActionId": "31wR7kM2nP5qT8vXyZaBcDeFgHj",
        "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
        "actionCode": "FORMATION_NAME_OPTIONS_EXHAUSTED",
        "actionName": "New company names needed",
        "status": "delivered",
        "reason": "The state rejected “Acme Holdings LLC”. Submit new company name options to continue the formation.",
        "open": true,
        "awaitingResponse": true,
        "updatedAt": "2026-08-13T17:33:12.751020"
      }
    ],
    "page": 0,
    "size": 20,
    "total": 1,
    "totalPages": 1
  },
  "error": null
}
```

Closed actions are excluded. `total` is the tenant-wide open count whatever page size you ask for, so `size=1` is a cheap "is anything waiting on me?" check. `size` is capped at 100 — a larger value is clamped rather than rejected.

### One company

```bash theme={null}
curl https://api.test.doola.com/v1/partner/companies/{companyId}/required-actions \
  -H "Authorization: dk_test_your_api_key_here"
```

```json theme={null}
{
  "payload": [
    {
      "requiredActionId": "31wR7kM2nP5qT8vXyZaBcDeFgHj",
      "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
      "actionCode": "FORMATION_NAME_OPTIONS_EXHAUSTED",
      "actionName": "New company names needed",
      "status": "resolved",
      "reason": "The state rejected “Acme Holdings LLC”. Submit new company name options to continue the formation.",
      "open": false,
      "awaitingResponse": false,
      "updatedAt": "2026-08-14T09:02:44.118220"
    }
  ],
  "error": null
}
```

A plain array under `payload`, unpaged, most recently changed first. Unlike the open list this one keeps `resolved` and `rejected` entries, which is what lets you tell "nothing was ever needed" apart from "something was needed and I missed it".

### One action, with history

```bash theme={null}
curl https://api.test.doola.com/v1/partner/companies/{companyId}/required-actions/{requiredActionId} \
  -H "Authorization: dk_test_your_api_key_here"
```

Adds a `history` array: every step the action has been through, oldest first, including whether doola reached your endpoint and what you submitted.

```json theme={null}
{
  "payload": {
    "requiredActionId": "31wR7kM2nP5qT8vXyZaBcDeFgHj",
    "doolaCompanyId": "31pLD0Tq2lm2FsgRBoHv4x3BpzZ",
    "actionCode": "FORMATION_NAME_OPTIONS_EXHAUSTED",
    "actionName": "New company names needed",
    "status": "submitted",
    "reason": "The state rejected “Acme Holdings LLC”. Submit new company name options to continue the formation.",
    "open": true,
    "awaitingResponse": false,
    "updatedAt": "2026-08-13T17:33:32.624556",
    "history": [
      { "status": "created", "createdAt": "2026-08-13T17:33:12.502357" },
      { "status": "delivered", "createdAt": "2026-08-13T17:33:12.751020" },
      {
        "status": "submitted",
        "submittedPayload": {
          "actionCode": "FORMATION_NAME_OPTIONS_EXHAUSTED",
          "nameOptions": [{ "name": "Acme Ventures", "entityTypeEnding": "LLC" }]
        },
        "createdAt": "2026-08-13T17:33:32.624556"
      }
    ]
  },
  "error": null
}
```

<Note>
  `submittedPayload` appears on `submitted` steps only. It is a record of what you sent at that moment, not the company's current state — read the company for that.
</Note>

## Reconciling

Webhooks can be missed — downtime on your side, or an endpoint doola disabled after repeated failures. An action left in `delivery_failed` is still open and still blocking that company, and no further webhook announces it.

Guard against that with one call at startup and on a schedule:

```bash theme={null}
curl "https://api.test.doola.com/v1/partner/required-actions?size=100" \
  -H "Authorization: dk_test_your_api_key_here"
```

Anything there you have no record of is work you missed. Compare `total` against what you received and page with `page=1`, `page=2`, and so on if you ever hold more than 100 open actions at once; for most tenants this is a single request no matter how many companies you have.

## Testing

The [sandbox playground](/api/sandbox-playground) cannot raise a required action today. To exercise the flow end to end in sandbox, ask doola to raise one on a sandbox company at [engineering@doola.com](mailto:engineering@doola.com) — the webhook, the reads, and the resolution endpoint then behave exactly as they do in production. The reads are safe to build against meanwhile: a company with nothing outstanding returns an empty list.

## Errors

Branch on `error.code`, never on the message. The 400-level codes below come from [submitting a resolution](/api/required-actions/name-options#submitting-new-names); the 404s are returned by the reads as well.

| HTTP | Code | Meaning |
| - | - | - |
| 400 | `E_VALIDATION_FAILED` | Body shape is wrong: `actionCode` missing, or `nameOptions` missing, empty, more than three, or carrying an unrecognised `entityTypeEnding`. The `error.fields.nameOptions` detail names the rule that failed |
| 400 | `E_REQUIRED_ACTION_CODE_MISMATCH` | `actionCode` does not match the action you are resolving — you likely mixed up two open actions |
| 400 | `E_NAME_OPTIONS_INVALID` | A recognised ending that does not belong to the company's entity type, for example a CCorp ending on an LLC |
| 404 | `E_REQUIRED_ACTION_NOT_FOUND` | No action with this ID on this company, including an action that belongs to another partner |
| 404 | `E_COMPANY_NOT_FOUND` | No company with this ID belongs to your tenant |
| 409 | `E_REQUIRED_ACTION_CLOSED` | The action is already resolved or rejected. A closed action never reopens; wait for the new one |
| 422 | `E_REQUIRED_ACTION_NOT_RESOLVABLE` | This action is resolved by another flow, such as an [SS-4 re-signature](/api/required-actions/ss4-reset) or a [Form 8821 signature](/api/required-actions/form8821-required) |

Checks run in a fixed order — body shape, company scope, action scope, `actionCode` match, resolvability, then whether the action is still open — so a request with more than one problem reports the first failure in that list. A closed signature action therefore answers `422`, not `409`.

Anything that is not yours is a `404`, never a `403` — a `403` would confirm the ID exists, which is enough to enumerate another partner's companies.

See [Errors](/api/errors) for the response envelope and the global codes that any endpoint can return.


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