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.
Endpoint
POST https://mcp.doow.co/mcp/onboardingAuthentication
Onboarding uses standard MCP OAuth tokens or API keys:
Authorization: Bearer <mcp_access_token>After signup, call signup_claim_tokens to get an MCP token for onboarding.
Tools
| Tool | Type | Confirm | Description |
|---|---|---|---|
onboarding_status | query | No | Read authoritative onboarding stages |
onboarding_next_step | query | No | Server-derived next step recommendation |
onboarding_start_connection | mutation | No | Start provider handoff, get single-use handoff_ref (30 min) |
onboarding_connection_status | query | No | Provider connection state |
onboarding_workspace_draft | mutation | Yes | Save/read company metadata draft |
onboarding_kyc_status | query | No | Normalized KYC status |
onboarding_kyc_requirements | query | No | Required KYC steps by country |
onboarding_start_kyc | mutation | No | Start KYC, return browser URL |
onboarding_stage_advance | mutation | Yes | Advance one confirmed stage |
onboarding_complete | mutation | Yes | Complete onboarding |
onboarding_plan_status | query | No | Subscription status and suggested tier |
onboarding_plan_options | query | No | Eligible plans with pricing |
onboarding_start_plan_selection | mutation | Yes | Start Stripe checkout |
onboarding_contact_sales | query | No | Enterprise inquiry status/URL |
Stage order
The server recommends this order via onboarding_next_step:
IdP → Expense (Accounting or Banking) → HRIS → Workspace → KYC → PlanIdP must be completed before any post-IdP stages, and at least one expense stage (Accounting or Banking) is required for onboarding_complete.
| Stage | Description | Required |
|---|---|---|
| IdP | Identity provider (Google, Microsoft, Zoho, Okta, OneLogin) | Yes |
| Accounting | Accounting provider (QuickBooks, Xero, etc.) | One of these |
| Banking | Banking provider (Plaid) | One of these |
| HRIS | HR integration (Gusto, Deel, BambooHR, etc.) | No |
| Workspace | Company metadata setup | Yes |
| KYC | Identity verification (browser-only) | Conditional (see below) |
| Plan | Subscription plan selection | Required 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
-
Initialize MCP session
Every MCP session requires initialization. The server returns a session ID for subsequent requests.
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:
Mcp-Session-Id: 3ab45ea9-367e-4e6c-86ee-fc8984ccaa4dSave the
Mcp-Session-Idheader. All subsequent calls need it. -
Check onboarding status
{"jsonrpc": "2.0","id": 2,"method": "tools/call","params": { "name": "onboarding_status", "arguments": {}}}Include headers:
Authorization: Bearer <mcp_token>Mcp-Session-Id: <session_id>Accept: application/json, text/event-streamResponse:
{"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 frombusiness_name),country_of_incorporation,default_spending_currency,financial_year_start, andinactivity_period(a number of days). A field with no value is left out of the response rather than returned asnull. -
Connect providers
For each provider, the agent starts a connection and the user completes OAuth in the browser.
Start connection:
{"jsonrpc": "2.0","id": 3,"method": "tools/call","params": { "name": "onboarding_start_connection", "arguments": { "provider": "google" }}}Response:
{"provider": "google","state": "PENDING","browser_required": true,"handoff_ref": "hs_onboarding_abc123","expires_at": "2026-09-21T14:30:00Z"}Open browser for user:
open "https://app.doow.co/onboarding#headless_handoff=hs_onboarding_abc123"What the browser does with
handoff_refThe browser client reads
headless_handofffrom the URL fragment and spends the token once:POST /v1/auth/headless/onboarding/handoff/exchangeContent-Type: application/json{ "handoff_token": "hs_onboarding_abc123" }Response:
{"provider": "google","expires_at": "2026-09-21T14:30:00Z"}The exchange returns only
providerandexpires_at: never a browser URL, organization ID, or member ID. The token is single-use and short-lived:handoff_refis valid for 30 minutes from issue.- A successful exchange moves the handoff from
ISSUEDtoSUCCEEDED. Exchanging the same token again returns401. - If the user abandons the browser step, call
onboarding_start_connectionagain to issue a freshhandoff_ref.
Poll connection status every 2-3 seconds while the user is in the browser:
{"jsonrpc": "2.0","id": 4,"method": "tools/call","params": { "name": "onboarding_connection_status", "arguments": { "provider": "google" }}}Response:
{"providers": [ { "provider": "google", "state": "CONFIRMED" }]} -
Configure workspace
Save company metadata with
onboarding_workspace_draft:{"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:
{"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 arrayonboarding_statusreturns. Fields you do not set stay omitted, which is whytrade_nameandinactivity_perioddo not appear above. -
Advance stages
After each step completes, advance the stage:
{"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_stepmatches the stage you are advancing, so connect the provider and let the browser step finish first. The server rejectsCOMPLETEDoutright, 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 genericSERVICE_UNAVAILABLErather than a code that names the missing connection.Response:
{"stages": ["LANDING", "IdP"]} -
Complete KYC
Check KYC requirements and status:
{"jsonrpc": "2.0","id": 7,"method": "tools/call","params": { "name": "onboarding_kyc_status", "arguments": {}}}Response:
{"status": "NOT_STARTED", "has_kyc": false}Start KYC:
{"jsonrpc": "2.0","id": 8,"method": "tools/call","params": { "name": "onboarding_start_kyc", "arguments": {}}}Response:
{"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 pollonboarding_kyc_statusuntilPENDINGorAPPROVED. -
Complete onboarding
Read
onboarding_statusandonboarding_kyc_status, check them against the requirements below, then call:{"jsonrpc": "2.0","id": 9,"method": "tools/call","params": { "name": "onboarding_complete", "arguments": {}}}onboarding_completerequires:stagesto includeIdP,WORKSPACE, and at least one ofACCOUNTINGorBANKINGonboarding_kyc_statusto reporthas_kyc: truewith statusPENDINGorAPPROVED, unless the user reached the browser-skip path below
Response:
{"status": "COMPLETED","stages": ["LANDING", "IdP", "ACCOUNTING", "WORKSPACE", "COMPLETED"]}A successful call returns only
statusandstages. It carries nocompletion_sourceand nokyc_verifiedfield.Do not gate on
missing_steps: it is computed over every possible stage, so it reportsHRISeven thoughHRISis optional andonboarding_completedoes not require it.Browser skip is the one exception. When the organization's stages already include
COMPLETEDand KYC status isNOT_STARTED, the user finished onboarding in the browser, and the call returns the skip details alongside the samestatus:{"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, orUNKNOWN) fails, as does a missing KYC record when the stages do not already includeCOMPLETED. An agent that starts KYC and then abandons it leaves the flow in a state thatonboarding_completerejects.Plan selection is not part of
onboarding_complete, so drive straight into it once the call succeeds (see Plan selection).
Connection states
| State | Meaning | Action |
|---|---|---|
CONFIRMED | Connected and verified | Proceed to next step |
PENDING | Connection in progress | Keep polling |
UNKNOWN | State not yet determined | Keep polling |
FAILED_RETRYABLE | Temporary failure | Retry with start_connection |
FAILED_TERMINAL | Permanent failure | Show error |
EXPIRED | Connection expired | Restart connection |
ACTIVE | Integration active | Provider working |
INACTIVE | Integration disabled | May need reactivation |
KYC statuses
| Status | Meaning |
|---|---|
NOT_STARTED | KYC not initiated |
PENDING | Verification in progress |
APPROVED | KYC passed |
REJECTED | KYC failed |
REQUIRES_ACTION | User action needed |
UNKNOWN | Status not yet determined |
Plan selection
Plan selection sets the organization's subscription tier and runs after onboarding_complete.
Check plan status
{"jsonrpc": "2.0","id": 10,"method": "tools/call","params": { "name": "onboarding_plan_status", "arguments": {}}}Response:
{"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.
| Status | What it means | What to do |
|---|---|---|
PENDING | No plan has been chosen yet | Keep polling onboarding_plan_status |
TRIALING | The user started a free trial | Done, the user can reach the dashboard |
ACTIVE | A paid subscription is live | Done, the user can reach the dashboard |
PAST_DUE | A payment failed | Run onboarding_start_plan_selection again to get a new checkout URL |
CANCELLED | The subscription was cancelled | Run onboarding_start_plan_selection again to get a new checkout URL |
SUSPENDED | A payment stayed unpaid past the dunning deadline | Run onboarding_start_plan_selection again to get a new checkout URL |
Get available plans
{"jsonrpc": "2.0","id": 11,"method": "tools/call","params": { "name": "onboarding_plan_options", "arguments": {}}}Response:
{"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
{"jsonrpc": "2.0","id": 12,"method": "tools/call","params": { "name": "onboarding_start_plan_selection", "arguments": { "tier": "GROWTH", "billing_cycle": "MONTHLY" }}}Response:
{"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:
{"jsonrpc": "2.0","id": 13,"method": "tools/call","params": { "name": "onboarding_contact_sales", "arguments": {}}}Response when no inquiry exists yet:
{"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:
{"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
-
IdP (Identity Provider)
Provider Key Google Workspace googleMicrosoft microsoftZoho Directory zohoDirectoryOkta oktaOneLogin onelogin -
Accounting
Provider Key QuickBooks quickbooksXero xeroZoho Books zohobooksSage sageBusinessCloudAccountingNetSuite netsuite -
Banking
Provider Key Plaid plaid -
HRIS
Provider Key Gusto gustoDeel deelBambooHR bamboohrZoho People zohoPeople
Confirmation requirements
These tools require MCP elicitation (interactive confirmation):
| Tool | Why |
|---|---|
onboarding_workspace_draft | Saves company metadata |
onboarding_stage_advance | Advances onboarding state |
onboarding_complete | Finalizes onboarding |
onboarding_start_plan_selection | Initiates 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
| Code | Meaning |
|---|---|
VALIDATION_ERROR | Invalid input parameters |
TOKEN_INVALID | Token signature or format is invalid |
TOKEN_EXPIRED | Access token expired, refresh required |
TOKEN_REVOKED | Token has been revoked |
SCOPE_INSUFFICIENT | Missing required scope |
CAPABILITY_REQUIRED | Client lacks elicitation support, so it cannot run a confirmation-gated tool |
ELICITATION_DENIED | User declined the confirmation prompt |
ELICITATION_TIMEOUT | Confirmation prompt timed out |
IDEMPOTENCY_CONFLICT | A request with this JSON-RPC ID is already in progress |
RATE_LIMITED | Too many requests |
SERVICE_UNAVAILABLE | Dependency service unavailable |
RESOURCE_NOT_FOUND | Resource does not exist |
SESSION_NOT_FOUND | MCP session expired or invalid |