DocsDocs
Open app

Headless Onboarding

Agent-directed workspace setup with 14 MCP tools for integrations, workspace config, and KYC.

After signup, the onboarding flow lets an agent coordinate connecting integrations, workspace setup, and KYC verification while keeping provider OAuth and identity verification in secure browser handoffs.

Status Check

Check progress and get the next step.

Server recommends stage order

Provider Connection

Connect identity, expense, and HRIS providers.

Browser handles OAuth while agent monitors

Workspace & KYC

Save company details and verify identity.

Metadata configured and identity verified

Plan & Complete

Select subscription plan and finish onboarding.

Workspace ready with chosen plan

Endpoint

text
POST https://mcp.doow.co/mcp/onboarding

Authentication

Onboarding uses standard MCP OAuth tokens or API keys:

text
Authorization: Bearer <mcp_access_token>

After signup, call signup_claim_tokens to get an MCP token for onboarding.

Tools

ToolTypeConfirmDescription
onboarding_statusqueryNoRead authoritative onboarding stages
onboarding_next_stepqueryNoServer-derived next step recommendation
onboarding_start_connectionmutationNoStart provider handoff, get single-use handoff_ref (30 min)
onboarding_connection_statusqueryNoProvider connection state
onboarding_workspace_draftmutationYesSave/read company metadata draft
onboarding_kyc_statusqueryNoNormalized KYC status
onboarding_kyc_requirementsqueryNoRequired KYC steps by country
onboarding_start_kycmutationNoStart KYC, return browser URL
onboarding_stage_advancemutationYesAdvance one confirmed stage
onboarding_completemutationYesComplete onboarding
onboarding_plan_statusqueryNoSubscription status and suggested tier
onboarding_plan_optionsqueryNoEligible plans with pricing
onboarding_start_plan_selectionmutationYesStart Stripe checkout
onboarding_contact_salesqueryNoEnterprise inquiry status/URL

Stage order

The server recommends this order via onboarding_next_step:

text
IdP → Expense (Accounting or Banking) → HRIS → Workspace → KYC → Plan

IdP must be completed before any post-IdP stages, and at least one expense stage (Accounting or Banking) is required for onboarding_complete.

StageDescriptionRequired
IdPIdentity provider (Google, Microsoft, Zoho, Okta, OneLogin)Yes
AccountingAccounting provider (QuickBooks, Xero, etc.)One of these
BankingBanking provider (Plaid)One of these
HRISHR integration (Gusto, Deel, BambooHR, etc.)No
WorkspaceCompany metadata setupYes
KYCIdentity verification (browser-only)Conditional (see below)
PlanSubscription plan selectionRequired for dashboard access

KYC can be skipped, but only through the browser-skip path. Step 7 of Onboarding steps below covers how that affects onboarding_complete.

Onboarding steps

  1. Initialize MCP session

    Every MCP session requires initialization. The server returns a session ID for subsequent requests.

    bash
    curl -s -i https://mcp.doow.co/mcp/onboarding \-H "Authorization: Bearer <mcp_token>" \-H "Accept: application/json, text/event-stream" \-H "Content-Type: application/json" \-d '{  "jsonrpc": "2.0",  "id": 1,  "method": "initialize",  "params": {    "protocolVersion": "2025-03-26",    "capabilities": {},    "clientInfo": {"name": "my-agent", "version": "1.0"}  }}'

    Response headers:

    text
    Mcp-Session-Id: 3ab45ea9-367e-4e6c-86ee-fc8984ccaa4d

    Save the Mcp-Session-Id header. All subsequent calls need it.

  2. Check onboarding status

    json
    {"jsonrpc": "2.0","id": 2,"method": "tools/call","params": {  "name": "onboarding_status",  "arguments": {}}}

    Include headers:

    text
    Authorization: Bearer <mcp_token>Mcp-Session-Id: <session_id>Accept: application/json, text/event-stream

    Response:

    json
    {"stages": ["LANDING"],"business_name": "Acme Inc","missing_steps": ["IdP", "ACCOUNTING", "BANKING", "HRIS", "WORKSPACE"],"next_step": "IdP","browser_required": true}

    The response also carries the other workspace fields once they are set: website_url, trade_name (the name the business trades under, which can differ from business_name), country_of_incorporation, default_spending_currency, financial_year_start, and inactivity_period (a number of days). A field with no value is left out of the response rather than returned as null.

  3. Connect providers

    For each provider, the agent starts a connection and the user completes OAuth in the browser.

    Start connection:

    json
    {"jsonrpc": "2.0","id": 3,"method": "tools/call","params": {  "name": "onboarding_start_connection",  "arguments": {    "provider": "google"  }}}

    Response:

    json
    {"provider": "google","state": "PENDING","browser_required": true,"handoff_ref": "hs_onboarding_abc123","expires_at": "2026-09-21T14:30:00Z"}

    Open browser for user:

    bash
    open "https://app.doow.co/onboarding#headless_handoff=hs_onboarding_abc123"

    What the browser does with handoff_ref

    The browser client reads headless_handoff from the URL fragment and spends the token once:

    text
    POST /v1/auth/headless/onboarding/handoff/exchangeContent-Type: application/json{ "handoff_token": "hs_onboarding_abc123" }

    Response:

    json
    {"provider": "google","expires_at": "2026-09-21T14:30:00Z"}

    The exchange returns only provider and expires_at: never a browser URL, organization ID, or member ID. The token is single-use and short-lived:

    • handoff_ref is valid for 30 minutes from issue.
    • A successful exchange moves the handoff from ISSUED to SUCCEEDED. Exchanging the same token again returns 401.
    • If the user abandons the browser step, call onboarding_start_connection again to issue a fresh handoff_ref.

    Poll connection status every 2-3 seconds while the user is in the browser:

    json
    {"jsonrpc": "2.0","id": 4,"method": "tools/call","params": {  "name": "onboarding_connection_status",  "arguments": {    "provider": "google"  }}}

    Response:

    json
    {"providers": [  {    "provider": "google",    "state": "CONFIRMED"  }]}
  4. Configure workspace

    Save company metadata with onboarding_workspace_draft:

    json
    {"jsonrpc": "2.0","id": 5,"method": "tools/call","params": {  "name": "onboarding_workspace_draft",  "arguments": {    "business_name": "Acme Inc",    "website_url": "https://acme.com",    "country_of_incorporation": "US",    "default_spending_currency": "USD",    "financial_year_start": "January"  }}}

    Response:

    json
    {"business_name": "Acme Inc","website_url": "https://acme.com","country_of_incorporation": "US","default_spending_currency": "USD","financial_year_start": "January","stages": ["LANDING"]}

    The response echoes the stored metadata and adds stages, the same array onboarding_status returns. Fields you do not set stay omitted, which is why trade_name and inactivity_period do not appear above.

  5. Advance stages

    After each step completes, advance the stage:

    json
    {"jsonrpc": "2.0","id": 6,"method": "tools/call","params": {  "name": "onboarding_stage_advance",  "arguments": {    "stage": "IdP"  }}}

    Valid stages: IdP, ACCOUNTING, BANKING, HRIS, WORKSPACE. Send exactly one stage per call, because the server rejects a request that carries more or fewer.

    The call only succeeds when the organization already has an active integration whose onboarding_step matches the stage you are advancing, so connect the provider and let the browser step finish first. The server rejects COMPLETED outright, since it sets that stage itself once every requirement is met. A call made before any connection exists fails as well, and the client sees the generic SERVICE_UNAVAILABLE rather than a code that names the missing connection.

    Response:

    json
    {"stages": ["LANDING", "IdP"]}
  6. Complete KYC

    Check KYC requirements and status:

    json
    {"jsonrpc": "2.0","id": 7,"method": "tools/call","params": {  "name": "onboarding_kyc_status",  "arguments": {}}}

    Response:

    json
    {"status": "NOT_STARTED", "has_kyc": false}

    Start KYC:

    json
    {"jsonrpc": "2.0","id": 8,"method": "tools/call","params": {  "name": "onboarding_start_kyc",  "arguments": {}}}

    Response:

    json
    {"kyc_url": "https://app.doow.co/onboarding/kyc","browser_required": true,"status": "NOT_STARTED"}

    KYC returns a direct kyc_url (no exchange needed), so open it for the user and poll onboarding_kyc_status until PENDING or APPROVED.

  7. Complete onboarding

    Read onboarding_status and onboarding_kyc_status, check them against the requirements below, then call:

    json
    {"jsonrpc": "2.0","id": 9,"method": "tools/call","params": {  "name": "onboarding_complete",  "arguments": {}}}

    onboarding_complete requires:

    • stages to include IdP, WORKSPACE, and at least one of ACCOUNTING or BANKING
    • onboarding_kyc_status to report has_kyc: true with status PENDING or APPROVED, unless the user reached the browser-skip path below

    Response:

    json
    {"status": "COMPLETED","stages": ["LANDING", "IdP", "ACCOUNTING", "WORKSPACE", "COMPLETED"]}

    A successful call returns only status and stages. It carries no completion_source and no kyc_verified field.

    Do not gate on missing_steps: it is computed over every possible stage, so it reports HRIS even though HRIS is optional and onboarding_complete does not require it.

    Browser skip is the one exception. When the organization's stages already include COMPLETED and KYC status is NOT_STARTED, the user finished onboarding in the browser, and the call returns the skip details alongside the same status:

    json
    {"status": "COMPLETED","completion_source": "BROWSER_SKIP","kyc_status": "NOT_STARTED","kyc_verified": false,"stages": ["LANDING", "IdP", "ACCOUNTING", "WORKSPACE", "COMPLETED"]}

    Any other KYC state (REJECTED, REQUIRES_ACTION, or UNKNOWN) fails, as does a missing KYC record when the stages do not already include COMPLETED. An agent that starts KYC and then abandons it leaves the flow in a state that onboarding_complete rejects.

    Plan selection is not part of onboarding_complete, so drive straight into it once the call succeeds (see Plan selection).

Connection states

StateMeaningAction
CONFIRMEDConnected and verifiedProceed to next step
PENDINGConnection in progressKeep polling
UNKNOWNState not yet determinedKeep polling
FAILED_RETRYABLETemporary failureRetry with start_connection
FAILED_TERMINALPermanent failureShow error
EXPIREDConnection expiredRestart connection
ACTIVEIntegration activeProvider working
INACTIVEIntegration disabledMay need reactivation

KYC statuses

StatusMeaning
NOT_STARTEDKYC not initiated
PENDINGVerification in progress
APPROVEDKYC passed
REJECTEDKYC failed
REQUIRES_ACTIONUser action needed
UNKNOWNStatus not yet determined

Plan selection

Plan selection sets the organization's subscription tier and runs after onboarding_complete.

Check plan status

json
{"jsonrpc": "2.0","id": 10,"method": "tools/call","params": {  "name": "onboarding_plan_status",  "arguments": {}}}

Response:

json
{"subscription_status": "PENDING","estimated_monthly_spend_usd": 4200,"suggested_tier": "GROWTH","eligible_tiers": ["GROWTH", "BUSINESS", "ENTERPRISE"],"enterprise_required": false}

eligible_tiers is every tier at or above suggested_tier. When suggested_tier is ENTERPRISE, enterprise_required is true and the response also carries a message directing the user to sales.

subscription_status tells you how far the user has got. The user reaches the dashboard only at TRIALING or ACTIVE.

StatusWhat it meansWhat to do
PENDINGNo plan has been chosen yetKeep polling onboarding_plan_status
TRIALINGThe user started a free trialDone, the user can reach the dashboard
ACTIVEA paid subscription is liveDone, the user can reach the dashboard
PAST_DUEA payment failedRun onboarding_start_plan_selection again to get a new checkout URL
CANCELLEDThe subscription was cancelledRun onboarding_start_plan_selection again to get a new checkout URL
SUSPENDEDA payment stayed unpaid past the dunning deadlineRun onboarding_start_plan_selection again to get a new checkout URL

Get available plans

json
{"jsonrpc": "2.0","id": 11,"method": "tools/call","params": {  "name": "onboarding_plan_options",  "arguments": {}}}

Response:

json
{"plans": [  {    "tier": "STARTER",    "monthly_cents": 0,    "annual_cents": 0,    "currency": "usd",    "eligible": false,    "contact_sales": false  },  {    "tier": "GROWTH",    "monthly_cents": 9900,    "annual_cents": 99000,    "currency": "usd",    "eligible": true,    "contact_sales": false  },  {    "tier": "BUSINESS",    "monthly_cents": 29900,    "annual_cents": 299000,    "currency": "usd",    "eligible": true,    "contact_sales": false  },  {    "tier": "ENTERPRISE",    "monthly_cents": null,    "annual_cents": null,    "currency": "usd",    "eligible": true,    "contact_sales": true  }],"suggested_tier": "GROWTH","enterprise_required": false,"ineligible_reason": "Your tracked spend requires GROWTH tier or above."}

Prices are integer cents, so 9900 is $99.00. The valid tiers are STARTER, GROWTH, BUSINESS, and ENTERPRISE. ENTERPRISE reports null prices and contact_sales: true, and it routes through onboarding_contact_sales rather than checkout. ineligible_reason is null when every tier is available.

Start plan selection

json
{"jsonrpc": "2.0","id": 12,"method": "tools/call","params": {  "name": "onboarding_start_plan_selection",  "arguments": {    "tier": "GROWTH",    "billing_cycle": "MONTHLY"  }}}

Response:

json
{"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...","browser_required": true}

Open checkout_url in the browser for the user. billing_cycle accepts MONTHLY or ANNUALLY and defaults to MONTHLY when you omit it. tier accepts STARTER, GROWTH, or BUSINESS, and it must be at or above suggested_tier, so choose from eligible_tiers rather than from the full list. ENTERPRISE is not selectable here, since it routes through onboarding_contact_sales. Requires confirmation.

Enterprise inquiries

For Enterprise tier, use onboarding_contact_sales:

json
{"jsonrpc": "2.0","id": 13,"method": "tools/call","params": {  "name": "onboarding_contact_sales",  "arguments": {}}}

Response when no inquiry exists yet:

json
{"contact_sales_url": "https://app.doow.co/contact-sales?source=onboarding","browser_required": true,"message": "Complete the Enterprise inquiry form to connect with our sales team."}

Response when an inquiry is already in progress:

json
{"inquiry_id": "inq_abc123","status": "SUBMITTED","already_submitted": true,"message": "An enterprise inquiry is already in progress. Our sales team will contact you."}

Supported providers

  1. IdP (Identity Provider)

    ProviderKey
    Google Workspacegoogle
    Microsoftmicrosoft
    Zoho DirectoryzohoDirectory
    Oktaokta
    OneLoginonelogin
  2. Accounting

    ProviderKey
    QuickBooksquickbooks
    Xeroxero
    Zoho Bookszohobooks
    SagesageBusinessCloudAccounting
    NetSuitenetsuite
  3. Banking

    ProviderKey
    Plaidplaid
  4. HRIS

    ProviderKey
    Gustogusto
    Deeldeel
    BambooHRbamboohr
    Zoho PeoplezohoPeople

Confirmation requirements

These tools require MCP elicitation (interactive confirmation):

ToolWhy
onboarding_workspace_draftSaves company metadata
onboarding_stage_advanceAdvances onboarding state
onboarding_completeFinalizes onboarding
onboarding_start_plan_selectionInitiates billing

Non-interactive clients (scripts, codex exec) cannot run these tools. A client with no elicitation support receives CAPABILITY_REQUIRED. When the client can elicit but the user declines the prompt, the call returns ELICITATION_DENIED instead. A prompt that goes unanswered returns ELICITATION_TIMEOUT.

Error codes

CodeMeaning
VALIDATION_ERRORInvalid input parameters
TOKEN_INVALIDToken signature or format is invalid
TOKEN_EXPIREDAccess token expired, refresh required
TOKEN_REVOKEDToken has been revoked
SCOPE_INSUFFICIENTMissing required scope
CAPABILITY_REQUIREDClient lacks elicitation support, so it cannot run a confirmation-gated tool
ELICITATION_DENIEDUser declined the confirmation prompt
ELICITATION_TIMEOUTConfirmation prompt timed out
IDEMPOTENCY_CONFLICTA request with this JSON-RPC ID is already in progress
RATE_LIMITEDToo many requests
SERVICE_UNAVAILABLEDependency service unavailable
RESOURCE_NOT_FOUNDResource does not exist
SESSION_NOT_FOUNDMCP session expired or invalid
Was this page helpful?