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

# How it works

> The pieces of an SDK integration, the keys that connect them, and what happens from the first screen to a formed company.

An SDK integration has three parts you own and three parts doola runs. Your server and your page never handle the founder's sensitive data, and doola never handles your checkout.

```mermaid theme={null}
flowchart LR
  subgraph yours["Yours"]
    page["Your page<br/>@doola/js"]
    server["Your server<br/>dk_ secret key"]
    checkout["Your checkout"]
  end
  subgraph doolas["doola"]
    loader["Loader<br/>js.doola.com"]
    frame["Embedded app (iframe)<br/>sdk.doola.com"]
    api["doola API<br/>api.doola.com"]
  end
  page -- "loads" --> loader
  loader -- "mounts" --> frame
  page -- "fetchAccessToken()" --> server
  server -- "mint a customer session" --> api
  frame -- "formation data, signatures" --> api
  frame -- "onFormed({ companyId })" --> page
  page --> checkout
  checkout -- "confirm payment" --> server
  server -- "payment-confirmed" --> api
  api -- "webhooks" --> server
```

1. **`@doola/js`** is a small package on your page with no UI of its own. It loads doola's loader from `js.doola.com`.
2. **The loader** mounts an iframe served from `sdk.doola.com` and passes it a short-lived customer session that your server minted.
3. **The embedded app** inside the iframe runs every screen: the formation wizard, the wait for your payment, document signing and the company dashboard. It talks to the doola API directly.
4. **Your server** holds your secret key. It mints customer sessions, reads the company before you charge, and confirms payment to doola.

## Keys and environments

You use two keys. They come as a pair, one for each environment.

| Key | Prefix | Lives in | Does |
| - | - | - | - |
| Publishable | `pk_test_` or `pk_live_` | Your frontend. Public by design | Identifies you, selects your branding and picks the environment for the iframe |
| Secret | `dk_test_` or `dk_live_` | Your server only. Never in a browser or a bundle | Calls the Partner API: mints sessions, reads companies, confirms payment |

Find both in the [Partner Portal](https://partners-portal.doola.com): the publishable key under **SDK → Install**, the secret key under **Settings → API Keys**. Your sandbox account gives you the test pair, and your production account the live one.

| Environment | Keys | Partner API | Embedded app |
| - | - | - | - |
| Test | `pk_test_`, `dk_test_` | `https://api.test.doola.com` | `https://sdk.test.doola.com` |
| Live | `pk_live_`, `dk_live_` | `https://api.doola.com` | `https://sdk.doola.com` |

Test and live are separate stacks with separate data. Never mix a test key with a live one. The loader always loads from `https://js.doola.com`, whichever key you use.

## Customer sessions

The iframe acts as one of your customers, never as you. Your server proves who that customer is by minting a **customer session** with your secret key, for the user who is signed in to your product.

* **Short-lived.** A session lasts minutes. The loader asks your route for a new one before the current one expires, so a doola session never outlives your own login.
* **Scoped to one customer.** The token can read and submit only that customer's formation. It cannot call the Partner API.
* **Minted on demand.** Your route needs no database. It reads your signed-in user and calls doola. See [Create sessions](/sdk/sessions).

### Who the customer is

doola identifies the customer from what your server sends when it mints the session:

* **`externalCustomerId`**, your own user id, is matched first. Send it: a customer who changes their email with you stays the same doola customer.
* **`email`** is matched next. A new email creates a new customer in your account.
* **Names, phone and country** are used only when doola creates the customer.
* An email that already belongs to a doola account outside your partner account is refused. The loader reports it as `email_in_use`.

### One company per customer

Each customer forms one company through the SDK. The iframe decides what to show from that customer's state, so you mount it the same way every time:

| The customer has | The iframe shows |
| - | - |
| No company | The formation wizard, resuming a saved draft if there is one |
| A company waiting on your payment | A screen asking them to complete payment, with a button that opens your checkout again |
| A paid company | The signing step if a signature is needed, then the company dashboard |
| A failed company | A message telling them to contact you, with no dashboard |
| A company doola cancelled before payment | The formation wizard, to start a new company |

## The formation lifecycle

```mermaid theme={null}
sequenceDiagram
  autonumber
  participant F as Founder
  participant P as Your page
  participant S as Your server
  participant D as doola
  P->>S: fetchAccessToken()
  S->>D: POST /v1/partner/customer-sessions
  D-->>S: accessToken, expiresIn
  S-->>P: accessToken, expiresIn
  F->>D: Completes the wizard in the iframe
  D-->>D: Company created as AWAITING_PAYMENT
  D-->>P: onFormed({ companyId })
  P->>S: Open checkout for companyId
  S->>D: GET company, check owner and status
  S->>S: Charge the founder
  S->>D: POST /v1/partner/companies/{companyId}/payment-confirmed
  D-->>D: Formation starts (PENDING)
  D-->>S: Webhooks as the company is formed
```

A company created through the SDK moves through the same `formationSubmissionStatus` values as one created through the Partner API, with one state before them:

| Status | Meaning |
| - | - |
| `AWAITING_PAYMENT` | Submitted in the SDK and waiting on your payment confirmation. Nothing is filed and nothing is charged to you |
| `PENDING` | You confirmed payment. doola received the formation and is processing it |
| `SUBMITTED` | Accepted and formation started |
| `COMPLETED` | The state filing is recorded |
| `FAILED` | The submission failed. Read `adminNotes` on the company and contact doola |
| `CANCELLED` | doola withdrew the formation at your request before payment was confirmed. Nothing was filed |

From `PENDING` on, everything works exactly as in the Partner API: the same [company object](/api/domain-objects#company), the same [webhooks](/api/webhooks), and the same [documents](/api/domain-objects#document).

## Where data goes

* **The founder's details, tax ids and signatures** are typed into the iframe and sent from there to doola. They never reach your page's JavaScript, your error tracking, your session replay or your logs.
* **Your page receives one value:** the `companyId`, through `onFormed`.
* **Your server reads what it needs** from the Partner API with your secret key: the company, its owner and the state fee.

See [Security and data](/sdk/security) for the full picture.


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