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

# Quickstart

> Embed doola company formation in your app, take a test payment and see the founder's company, in about 15 minutes.

By the end of this page, a signed-in user of your app can fill in the formation wizard, pay in your checkout and land on their company's dashboard, all in the test environment. Nothing is filed and nothing is charged.

## Before you start

You need:

* **Your publishable test key** (`pk_test_...`), from your sandbox account in the [Partner Portal](https://partners-portal.doola.com), under **SDK → Install**.
* **Your secret test key** (`dk_test_...`), from **Settings → API Keys** in the same account. Keep it on your server.
* **An app with sign-in** and a server that runs JavaScript. The steps use Next.js; the tabs show Express where it differs. For other languages, see [Create sessions](/sdk/sessions#any-other-language).

Put the secret key in your server's environment:

```bash .env theme={null}
DOOLA_API_KEY=dk_test_your_api_key_here
```

## 1. Install

```bash theme={null}
npm install @doola/js
```

## 2. Add a session route

This route creates a short-lived doola session for whoever is signed in to your app.

<Tabs>
  <Tab title="Next.js">
    ```ts app/doola-session/route.ts theme={null}
    import { createSessionHandler } from '@doola/js/server';

    export const POST = createSessionHandler({
      apiKey: process.env.DOOLA_API_KEY,
      getCustomer: async (request) => {
        const user = await getSignedInUser(request); // your own auth
        return user ? { email: user.email, externalCustomerId: user.id } : null;
      },
      onFailure: (failure) => console.error('doola session failed', failure),
    });
    ```
  </Tab>

  <Tab title="Express">
    ```ts theme={null}
    import { createCustomerSession } from '@doola/js/server';

    app.post('/doola-session', async (req, res, next) => {
      try {
        const user = await getSignedInUser(req); // your own auth
        if (!user) {
          res.status(401).end();
          return;
        }

        const { status, body, code, failure } = await createCustomerSession({
          apiKey: process.env.DOOLA_API_KEY,
          customer: { email: user.email, externalCustomerId: user.id },
        });

        if (failure) console.error('doola session failed', failure);

        res.set('Cache-Control', 'no-store');
        if (body) res.status(status).json(body);
        else if (code) res.status(status).json({ code });
        else res.status(status).end();
      } catch (error) {
        next(error);
      }
    });
    ```
  </Tab>
</Tabs>

## 3. Mount the SDK

Render the SDK on a page your signed-in user can reach. When they submit the wizard, `onFormed` hands you the new company's id.

```tsx app/formation/Formation.tsx theme={null}
'use client';

import { loadDoola } from '@doola/js';
import { useEffect, useRef, useState } from 'react';

export function Formation() {
  const container = useRef<HTMLDivElement>(null);
  const [checkoutFor, setCheckoutFor] = useState<string | null>(null);

  useEffect(() => {
    if (checkoutFor) return;

    let element: HTMLElement | undefined;
    let unmounted = false;

    loadDoola({
      publishableKey: 'pk_test_your_publishable_key',
      fetchAccessToken: async () => {
        const r = await fetch('/doola-session', { method: 'POST' });
        if (!r.ok) {
          // Reject with the status and doola's error code: the loader maps them to error types.
          const { code } = await r.json().catch(() => ({}));
          throw Object.assign(new Error('doola session'), { status: r.status, code });
        }
        return r.json();
      },
      onAuthError: (error) => console.error('doola', error),
      onFormed: ({ companyId }) => setCheckoutFor(companyId),
    }).then((doola) => {
      if (unmounted) return;

      element = doola.create();
      container.current?.append(element);
    });

    return () => {
      unmounted = true;
      element?.remove();
    };
  }, [checkoutFor]);

  if (checkoutFor) return <Checkout companyId={checkoutFor} onDone={() => setCheckoutFor(null)} />;

  return <div ref={container} />;
}
```

<Note>
  This keeps the quickstart short by passing `setCheckoutFor` straight to `loadDoola`. That works here because the component never unmounts. In a real app, keep one instance outside your components, as shown in [Embed the SDK](/sdk/embed#one-instance-per-page).
</Note>

## 4. Take payment and confirm it

Your checkout charges the founder, then your server tells doola. Until then, doola files nothing. For this test, the checkout is a single button.

```tsx app/formation/Checkout.tsx theme={null}
'use client';

export function Checkout({ companyId, onDone }: { companyId: string; onDone: () => void }) {
  async function pay() {
    const r = await fetch(`/api/checkout/${encodeURIComponent(companyId)}/pay`, { method: 'POST' });
    if (r.ok) onDone(); // mounts the SDK again, which now shows the founder's company
  }

  return <button onClick={pay}>Pay for your company</button>;
}
```

On your server, check that the company belongs to the signed-in user and still waits on payment, charge, then confirm:

```ts app/api/checkout/[companyId]/pay/route.ts theme={null}
const DOOLA_API = 'https://api.test.doola.com';
const headers = { authorization: process.env.DOOLA_API_KEY!, 'content-type': 'application/json' };

async function doola<T>(path: string, init?: RequestInit): Promise<T> {
  const r = await fetch(`${DOOLA_API}${path}`, { ...init, headers });
  if (!r.ok) throw new Error(`doola ${path} answered ${r.status}`);

  const text = await r.text(); // payment-confirmed answers with an empty body
  return text ? JSON.parse(text).payload : undefined;
}

export async function POST(request: Request, { params }: { params: Promise<{ companyId: string }> }) {
  const user = await getSignedInUser(request); // your own auth
  if (!user) return new Response(null, { status: 401 });

  const { companyId } = await params;
  const company = await doola<{ doolaCustomerId: string; formationSubmissionStatus: string }>(
    `/v1/partner/companies/${encodeURIComponent(companyId)}`,
  );
  const owner = await doola<{ email?: string }>(
    `/v1/partner/customers/${encodeURIComponent(company.doolaCustomerId)}`,
  );
  if (owner.email?.toLowerCase() !== user.email.toLowerCase()) return new Response(null, { status: 404 });

  const paid = ['PENDING', 'SUBMITTED', 'COMPLETED'].includes(company.formationSubmissionStatus);
  if (!paid && company.formationSubmissionStatus !== 'AWAITING_PAYMENT') {
    return new Response(null, { status: 409 }); // CANCELLED, FAILED or unknown: never charge
  }

  if (!paid) {
    // Charge the founder here with your payment provider: one idempotency key per attempt.
    await doola(`/v1/partner/companies/${encodeURIComponent(companyId)}/payment-confirmed`, {
      method: 'POST',
      body: JSON.stringify({ partnerReference: `test-${companyId}` }),
    });
  }

  return new Response(null, { status: 204 });
}
```

[Take payment](/sdk/payments) covers pricing, retries, refunds and everything else a real checkout needs.

## 5. Try it

1. Sign in to your app and open the page with the SDK. The wizard appears, in your branding if you set it in the portal.
2. Fill it in and press **Submit and pay**. Your checkout replaces the SDK.
3. Press **Pay for your company**. The SDK mounts again and shows the founder's company. Without an SSN, it first asks them to sign the IRS Form SS-4.
4. Read the company with your secret key: `formationSubmissionStatus` is now past `AWAITING_PAYMENT`.

<Check>
  That is a complete integration. Each customer forms one company, so sign in as a new user, or send a new email, to run it again.
</Check>

## Next steps

<CardGroup cols={2}>
  <Card title="Take payment" icon="credit-card" href="/sdk/payments">
    Price on your server, charge once per company, and handle every edge case.
  </Card>

  <Card title="Branding" icon="palette" href="/sdk/branding">
    Your logo, colors and font, set in the Partner Portal.
  </Card>

  <Card title="After payment" icon="bell" href="/sdk/after-payment">
    Webhooks, documents and signatures once filing starts.
  </Card>

  <Card title="Go-live checklist" icon="rocket" href="/sdk/go-live">
    Everything to check before you switch to live keys.
  </Card>
</CardGroup>


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