API & MCP Reference
Everything you need to integrate with Intake — via REST API or MCP.
Base URL:
https://dointake.com/api/v1
Introduction
Intake offers two integration paths: a REST API and an MCP server (Model Context Protocol). Both provide full access to clients, document requests, forms, and webhooks.
REST API Base URL: https://dointake.com/api/v1
MCP Server: Connect any MCP-compatible client (Claude, Cursor, etc.) — see the MCP Protocol section for setup.
All REST requests and responses use JSON. Timestamps are returned in ISO 8601 format. All resource IDs are opaque strings.
Plan limits:
| Free | Pro ($19/month) | Team ($49/month, 3 seats) | |
|---|---|---|---|
| Clients | 10 | Unlimited | Unlimited |
| Deals | 3 | Unlimited | Unlimited |
| Parties per deal | 5 | Unlimited | Unlimited |
| Workspace members | 1 | 1 | 3 included, +$15/seat |
| Per-member API keys & MCP | — | — | Yes |
Authentication
Authenticate by including your API key in the Authorization header as a Bearer token.
API keys are created under Avatar menu → Settings → API Keys. Each key has scopes that control access:
| Scope | Access |
|---|---|
read | List and retrieve resources |
write | Create and update resources |
webhooks | Manage webhook subscriptions |
Rate limiting: 120 requests per minute per API key. When exceeded, the API returns 429 with a Retry-After header.
Test vs. Live Keys
API keys have a mode that controls data access:
| Key Prefix | Mode | Data Access |
|---|---|---|
dk_live_... | Live | Production data only |
dk_test_... | Test | Sandbox data only |
Test keys create and access isolated sandbox data — your production data is never affected. Test keys also bypass plan limits, so you can test freely without upgrading.
Create test keys in Settings → API Keys by selecting "Test" mode.
Team workspaces
On the Team plan every person makes their own keys (and their own MCP connector
authorizations, choosing the workspace on the consent screen). A teammate's
credential acts as that person: their membership role, their visibility, and
their name in the activity log. Members see only the requests, deals, and clients
assigned to them; template and webhook management, and assignee_id, need the
admin role and return 403 forbidden otherwise. Credentials stop working the
moment the person leaves or is removed from the workspace.
/api/v1/me
Test your API key
Returns the workspace the credential acts in, your role, and `actor` — the person whose credential it is (`actor.id` is their member id, or null for the owner of a workspace that has never invited anyone).
curl https://dointake.com/api/v1/me \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": 1,
"name": "Your Name",
"email": "[email protected]",
"role": "admin",
"actor": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]"
},
"subscription_status": "active"
}
}
Errors
The API uses standard HTTP status codes. Errors return a JSON object with an error key containing code and message fields.
| Status | Meaning |
|---|---|
200 | Success |
201 | Created |
204 | Deleted (no content) |
401 | Unauthorized — missing or invalid API key |
403 | Forbidden — insufficient scope or plan limit exceeded |
404 | Not found |
422 | Validation error |
429 | Rate limited |
500 | Internal server error |
/api/v1/...
Error response format
All errors follow this structure. Validation errors include a `details` object mapping field names to error messages.
{
"error": {
"code": "validation_error",
"message": "Validation failed",
"details": {
"email": [
"can't be blank"
]
}
}
}
Pagination
List endpoints return all matching records. Use query parameters to filter results:
- Clients:
?search=termto filter by name or email;?external_id=x&external_source=yto match by external system;?tags=vip,residentialto filter by tags - Requests:
?status=pending&client_id=xxx&deal_id=xxx - Deals:
?status=active;?external_id=x&external_source=yto match by external system
Clients
Manage your clients. Clients are the people you collect documents from.
/api/v1/clients
List clients
Returns all clients for your account. Optionally filter by search term, external ID, or tags.
| Parameter | Type | Required | Description |
|---|---|---|---|
search |
string | optional | Filter by name or email |
external_id |
string | optional | Filter by external ID |
external_source |
string | optional | Filter by external source (used with external_id) |
tags |
string | optional | Filter by tags (comma-separated) |
curl https://dointake.com/api/v1/clients?search=jane \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "cl_abc123",
"name": "Jane Smith",
"metadata": {
"lead_score": 85
},
"email": "[email protected]",
"updated_at": "2025-01-15T10:30:00Z",
"tags": [
"vip",
"residential"
],
"created_at": "2025-01-15T10:30:00Z",
"phone": "+1234567890",
"external_id": "contact_123",
"external_source": "salesforce",
"preferred_language": "en",
"notification_preference": "email",
"sms_consent": null,
"email_undeliverable_at": null
}
]
}
/api/v1/clients/:id
Get a client
Retrieve a single client by their ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Client ID |
curl https://dointake.com/api/v1/clients/cl_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "cl_abc123",
"name": "Jane Smith",
"metadata": {
"lead_score": 85
},
"email": "[email protected]",
"updated_at": "2025-01-15T10:30:00Z",
"tags": [
"vip",
"residential"
],
"created_at": "2025-01-15T10:30:00Z",
"phone": "+1234567890",
"external_id": "contact_123",
"external_source": "salesforce",
"preferred_language": "en",
"notification_preference": "external",
"sms_consent": {
"method": "verbal",
"attested_at": "2025-01-15T10:28:00Z"
},
"email_undeliverable_at": null
}
}
/api/v1/clients
Create a client
Create a new client.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | required | Client's full name |
email |
string | required | Client's email address |
phone |
string | optional | Client's phone number |
external_id |
string | optional | External system ID |
external_source |
string | optional | External system name (e.g. salesforce, hubspot) |
metadata |
object | optional | Arbitrary key-value metadata |
tags |
array | optional | List of string tags |
preferred_language |
string | optional | Portal & email language: en, pl, nl, or es (default en) |
notification_preference |
string | optional | email (Intake emails the client, default) or external (your system delivers notifications, e.g. SMS from webhook events) |
sms_consent_method |
string | optional | How the client consented to texts: verbal, written, or portal. Setting it records an attestation (timestamp + recording user); sms_consent_attested_at may be supplied to backdate |
curl -X POST https://dointake.com/api/v1/clients \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Jane Smith", "email": "[email protected]"}'
{
"data": {
"id": "cl_abc123",
"name": "Jane Smith",
"metadata": {},
"email": "[email protected]",
"updated_at": "2025-01-15T10:30:00Z",
"tags": [],
"created_at": "2025-01-15T10:30:00Z",
"phone": null,
"external_id": null,
"external_source": null,
"preferred_language": "en",
"notification_preference": "email",
"sms_consent": null,
"email_undeliverable_at": null
}
}
/api/v1/clients/:id
Update a client
Update a client's name, email, phone, or metadata.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Client ID |
name |
string | optional | Client's full name |
email |
string | optional | Client's email address |
phone |
string | optional | Client's phone number |
external_id |
string | optional | External system ID |
external_source |
string | optional | External system name |
metadata |
object | optional | Arbitrary key-value metadata |
tags |
array | optional | List of string tags |
preferred_language |
string | optional | Portal & email language: en, pl, nl, or es (default en) |
notification_preference |
string | optional | email (Intake emails the client, default) or external (your system delivers notifications, e.g. SMS from webhook events) |
sms_consent_method |
string | optional | How the client consented to texts: verbal, written, or portal. Setting it records an attestation (timestamp + recording user); sms_consent_attested_at may be supplied to backdate |
curl -X PATCH https://dointake.com/api/v1/clients/cl_abc123 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Jane Doe", "phone": "+1987654321"}'
{
"data": {
"id": "cl_abc123",
"name": "Jane Doe",
"metadata": {},
"email": "[email protected]",
"updated_at": "2025-01-20T14:00:00Z",
"tags": [],
"created_at": "2025-01-15T10:30:00Z",
"phone": "+1987654321",
"external_id": null,
"external_source": null,
"preferred_language": "en",
"notification_preference": "email",
"sms_consent": null,
"email_undeliverable_at": null
}
}
/api/v1/clients/upsert
Upsert a client
Create or update a client by matching on `external_id` + `external_source`, or by `email` if no external ID is provided. Returns `201` if a new client was created, `200` if an existing client was updated.
| Parameter | Type | Required | Description |
|---|---|---|---|
email |
string | required | Client's email address |
name |
string | optional | Client's full name |
phone |
string | optional | Client's phone number |
external_id |
string | optional | External system ID (used for matching) |
external_source |
string | optional | External system name (used with external_id for matching) |
metadata |
object | optional | Arbitrary key-value metadata |
tags |
array | optional | List of string tags |
preferred_language |
string | optional | Portal & email language: en, pl, nl, or es (default en) |
notification_preference |
string | optional | email (Intake emails the client, default) or external (your system delivers notifications, e.g. SMS from webhook events) |
sms_consent_method |
string | optional | How the client consented to texts: verbal, written, or portal. Setting it records an attestation (timestamp + recording user); sms_consent_attested_at may be supplied to backdate |
curl -X PUT https://dointake.com/api/v1/clients/upsert \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "name": "Jane Smith", "external_id": "contact_123", "external_source": "salesforce"}'
{
"data": {
"id": "cl_abc123",
"name": "Jane Smith",
"metadata": {},
"email": "[email protected]",
"updated_at": "2025-01-15T10:30:00Z",
"tags": [],
"created_at": "2025-01-15T10:30:00Z",
"phone": null,
"external_id": "contact_123",
"external_source": "salesforce",
"preferred_language": "en",
"notification_preference": "email",
"sms_consent": null,
"email_undeliverable_at": null
}
}
Members
People in your workspace (Team plan). Requests and deals carry an assignee — the
member who receives the professional-facing emails (client uploaded, overdue,
never viewed). Member ids (mem_…) are what you pass as assignee_id when
creating, updating, or filtering requests and deals.
New work is routed by the rule in Settings → Team → Routing when no assignee_id
is given: whoever creates it (default), the client's owner, or the member with the
fewest open items.
/api/v1/members
List members
Returns everyone who can be assigned work, owner first. Solo workspaces return just the owner.
curl https://dointake.com/api/v1/members \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "mem_a1b2c3",
"name": "Your Name",
"email": "[email protected]",
"role": "owner"
},
{
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
}
]
}
Requests
Document requests are sent to clients to collect files and information. Each request has a checklist of items and collects uploads from the client.
/api/v1/requests
List requests
Returns all document requests. Filter by status, client, or deal.
| Parameter | Type | Required | Description |
|---|---|---|---|
status |
string | optional | Filter by status: draft, sent, in_progress, complete, archived (archived means canceled) |
client_id |
string | optional | Filter by client ID |
deal_id |
string | optional | Filter by deal ID |
assignee_id |
string | optional | Filter by workspace member (mem_…), or "unassigned" |
curl "https://dointake.com/api/v1/requests?status=sent" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "req_xyz789",
"status": "sent",
"title": "Tax Documents 2025",
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"completed_at": null,
"sent_at": "2025-01-20T09:00:00Z",
"created_at": "2025-01-15T10:30:00Z",
"due_date": "2025-04-15",
"assignee": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
},
"item_count": 5
}
]
}
/api/v1/requests/:id
Get a request
Retrieve a single request with its checklist items.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Request ID |
curl https://dointake.com/api/v1/requests/req_xyz789 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "req_xyz789",
"status": "sent",
"description": "Please upload your tax documents",
"title": "Tax Documents 2025",
"forms": [
{
"id": "sub_abc123",
"name": "Client Intake Form",
"status": "completed",
"completed_at": "2025-01-20T14:30:00Z",
"submitted_ip": "203.0.113.42",
"responses": [
{
"label": "Full Name",
"type": "text",
"value": "Jane Smith",
"field_id": "field_1"
},
{
"label": "Signature",
"signed": true,
"type": "signature",
"value": null,
"field_id": "field_2"
}
]
}
],
"items": [
{
"id": "item_001",
"name": "W-2 Form",
"position": 0,
"status": "pending",
"description": "From your employer",
"required": true,
"created_at": "2025-01-15T10:30:00Z",
"upload_count": 0
}
],
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"completed_at": null,
"sent_at": "2025-01-20T09:00:00Z",
"recipients": [],
"created_at": "2025-01-15T10:30:00Z",
"payments": [
{
"id": "pay_abc123",
"name": "Retainer",
"status": "paid",
"description": null,
"required": true,
"currency": "usd",
"created_at": "2025-02-01T12:00:00Z",
"amount_cents": 150000,
"paid_at": "2025-02-11T10:00:00Z",
"refunded_at": null
}
],
"due_date": "2025-04-15",
"deal_id": null,
"assignee": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
},
"max_reminders": 10,
"notify_professional_after_days": 7,
"reminder_frequency_days": null,
"reminder_count": 3,
"item_count": 1,
"last_reminder_at": "2025-02-10T09:00:00Z",
"next_reminder_at": "2025-02-12",
"pdf_forms": [
{
"id": "pfr_abc123",
"status": "completed",
"completed_at": "2025-01-20T15:00:00Z",
"template_name": "Engagement Letter",
"download_url": "https://dointake.com/downloads/tok_xyz789"
}
]
}
}
/api/v1/requests
Create a request
Create a new document request for a client. Optionally include checklist items and co-clients (additional recipients who each get their own portal link).
| Parameter | Type | Required | Description |
|---|---|---|---|
client_id |
string | required | Primary client ID to send the request to |
co_client_ids |
array | optional | Existing client IDs to add as co-recipients |
co_clients |
array | optional | Co-recipients to create inline as {name, email} objects (an existing client is reused when the email matches) |
title |
string | required | Request title |
description |
string | optional | Description or instructions |
due_date |
string | optional | Due date (YYYY-MM-DD) |
assignee_id |
string | optional | Workspace member to assign (mem_… from GET /members). Defaults to your workspace's routing rule. |
locale |
string | optional | Override portal/email language for this request: en, pl, nl, es (defaults to the client's language) |
items |
array | optional | Array of checklist items to create |
curl -X POST https://dointake.com/api/v1/requests \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"client_id": "cl_abc123", "title": "Tax Documents", "co_clients": [{"name": "John Smith", "email": "[email protected]"}], "items": [{"name": "W-2 Form"}]}'
{
"data": {
"id": "req_xyz789",
"status": "pending",
"description": null,
"title": "Tax Documents",
"items": [
{
"id": "item_001",
"name": "W-2 Form",
"position": 0,
"status": "pending",
"description": null,
"required": true,
"created_at": "2025-01-15T10:30:00Z",
"upload_count": 0
}
],
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"locale": null,
"completed_at": null,
"sent_at": null,
"recipients": [
{
"id": "cl_def456",
"name": "John Smith",
"email": "[email protected]",
"preferred_language": null
}
],
"created_at": "2025-01-15T10:30:00Z",
"due_date": null,
"deal_id": null,
"item_count": 1
}
}
/api/v1/requests/:id
Update a request
Update an existing document request.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Request ID |
title |
string | optional | Request title |
assignee_id |
string | optional | Reassign to a workspace member (mem_… from GET /members); empty string unassigns |
description |
string | optional | Description or instructions |
due_date |
string | optional | Due date (YYYY-MM-DD) |
locale |
string | optional | Override portal/email language for this request: en, pl, nl, es (defaults to the client's language) |
reminder_enabled |
boolean | optional | Enable email reminders |
reminder_frequency_days |
integer | optional | Days between reminders (1–30). Omit or null for the smart schedule: 2, 5, 9 days after sending, then weekly |
max_reminders |
integer | optional | Max reminders to send (1–100). Defaults to 8; null for unlimited. Reminders also stop the day after `due_date` with a final overdue notice, and resume if the due date is extended. |
notify_professional_after_days |
integer | optional | Notify professional if no response after N days (1–90, null to disable) |
curl -X PATCH https://dointake.com/api/v1/requests/req_xyz789 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title"}'
{
"data": {
"id": "req_xyz789",
"status": "sent",
"description": null,
"title": "Updated Title",
"items": [],
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"completed_at": null,
"sent_at": "2025-01-20T09:00:00Z",
"created_at": "2025-01-15T10:30:00Z",
"due_date": "2025-04-15",
"deal_id": null,
"item_count": 0
}
}
/api/v1/requests/:id/send
Send a request
Send a draft request to the client via email. Changes status from draft to sent. Set `skip_email` to activate the request without emailing the client (useful for sharing the portal link manually). Returns `422` with code `no_items` if the request has no documents, forms, PDFs, or payments for the client to act on.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Request ID |
skip_email |
boolean | optional | If true, activate the request without sending an email to the client. Default: false. |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/send \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "req_xyz789",
"status": "sent",
"title": "Tax Documents",
"items": [],
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"completed_at": null,
"sent_at": "2025-01-20T09:00:00Z",
"created_at": "2025-01-15T10:30:00Z",
"deal_id": null,
"item_count": 0
}
}
/api/v1/requests/:id/complete
Complete a request
Mark a document request as complete.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Request ID |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/complete \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "req_xyz789",
"status": "complete",
"title": "Tax Documents",
"items": [],
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"completed_at": "2025-02-01T14:00:00Z",
"created_at": "2025-01-15T10:30:00Z",
"deal_id": null,
"item_count": 0
}
}
/api/v1/requests/:id/cancel
Cancel a request
Cancel a document request. Sets its status to `archived`, closes the client's portal link immediately, and stops all reminder emails. No email is sent to the client. Reopen the request to make it active again.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Request ID |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/cancel \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "req_xyz789",
"status": "archived",
"title": "Tax Documents",
"items": [],
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"completed_at": null,
"created_at": "2025-01-15T10:30:00Z",
"deal_id": null,
"item_count": 0
}
}
/api/v1/requests/:id/reopen
Reopen a request
Reopen a completed or canceled document request.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Request ID |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/reopen \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "req_xyz789",
"status": "in_progress",
"title": "Tax Documents",
"items": [],
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"completed_at": null,
"created_at": "2025-01-15T10:30:00Z",
"deal_id": null,
"item_count": 0
}
}
/api/v1/requests/:request_id/reminders
List reminders
Returns the reminder audit trail for a request. Shows when each reminder was sent, to whom, and which items were pending. `recipient_type` is one of `client`, `co_client`, `professional_overdue` (the no-response alert), `professional_cap_reached` (the reminder-limit alert) `professional_never_viewed` (no portal view 3 days after sending) or `professional_due_passed` (the due date went by; reminders stopped). Note: `professional_overdue` and `professional_cap_reached` replaced a single `professional` value in August 2026.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
curl https://dointake.com/api/v1/requests/req_xyz789/reminders \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"sent_at": "2025-02-01T09:00:00Z",
"recipient_email": "[email protected]",
"urgency": "friendly",
"recipient_type": "client",
"pending_items": [
"W-2 Form",
"1099 Form"
]
},
{
"sent_at": "2025-02-04T09:00:00Z",
"recipient_email": "[email protected]",
"urgency": "urgent",
"recipient_type": "client",
"pending_items": [
"W-2 Form"
]
},
{
"sent_at": "2025-02-08T09:00:00Z",
"recipient_email": "[email protected]",
"urgency": "overdue",
"recipient_type": "professional_overdue",
"pending_items": []
}
]
}
/api/v1/requests/:request_id/recipients
Add a recipient
Add a co-client to an existing request. Each co-client gets their own portal link and is emailed when the request is sent. Pass either client_id for an existing client, or email (with an optional name) to create or reuse one.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
client_id |
string | optional | Existing client ID to add. Required unless email is given. |
email |
string | optional | Email of the co-client to create or reuse |
name |
string | optional | Name used when creating a new client from email |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/recipients \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "John Smith", "email": "[email protected]"}'
{
"data": {
"id": "cl_def456",
"name": "John Smith",
"email": "[email protected]",
"preferred_language": null
}
}
/api/v1/requests/:request_id/recipients/:client_id
Remove a recipient
Remove a co-client from a request, invalidating their portal link. Returns 404 for the primary client, who cannot be removed this way.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
client_id |
string | required | Client ID of the co-client to remove |
curl -X DELETE https://dointake.com/api/v1/requests/req_xyz789/recipients/cl_def456 \
-H "Authorization: Bearer YOUR_API_KEY"
204 No Content
Checklist Items
Checklist items are individual documents or pieces of information requested within a document request.
/api/v1/requests/:request_id/items
List checklist items
Returns all checklist items for a given request.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
curl https://dointake.com/api/v1/requests/req_xyz789/items \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "item_001",
"name": "W-2 Form",
"position": 0,
"status": "pending",
"description": "From your employer",
"required": true,
"created_at": "2025-01-15T10:30:00Z",
"review_note": null,
"review_status": null,
"upload_count": 0
}
]
}
/api/v1/requests/:request_id/items
Create a checklist item
Add a new checklist item to a request.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
name |
string | required | Item name |
description |
string | optional | Instructions for the client |
required |
boolean | optional | Whether this item is required (default: true) |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/items \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "W-2 Form", "description": "From your employer"}'
{
"data": {
"id": "item_002",
"name": "W-2 Form",
"position": 1,
"status": "pending",
"description": "From your employer",
"required": true,
"created_at": "2025-01-15T10:30:00Z",
"review_note": null,
"review_status": null,
"upload_count": 0
}
}
/api/v1/items/:id
Update item review status
Set the review status and optional note on a checklist item. Only works when the item's status is "received". Review status values are validated against the user's configured list (defaults: in_review, approved, rejected).
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Item ID |
review_status |
string | required | Review status (user-configured, defaults: in_review, approved, rejected) |
review_note |
string | optional | Optional note (e.g. reason for rejection) |
curl -X PATCH https://dointake.com/api/v1/items/itm_abc123 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"review_status": "in_review"}'
{
"data": {
"id": "itm_abc123",
"name": "W-2 Form",
"position": 0,
"status": "received",
"description": "From your employer",
"required": true,
"created_at": "2025-01-15T10:30:00Z",
"review_note": null,
"review_status": "in_review",
"upload_count": 1
}
}
/api/v1/items/:id/re-request
Re-request item
Re-request a checklist item. Resets status to pending and clears review_status; the review_note is kept and shown to the client on the portal (and emailed to them) until a replacement upload arrives. Previous uploads are kept for reference but marked `superseded` and excluded from exports.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Item ID |
curl -X POST https://dointake.com/api/v1/items/itm_abc123/re-request \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "itm_abc123",
"name": "W-2 Form",
"position": 0,
"status": "pending",
"description": "From your employer",
"required": true,
"created_at": "2025-01-15T10:30:00Z",
"review_note": null,
"review_status": null,
"upload_count": 1
}
}
Uploads
Uploads are files submitted by clients in response to document requests, or attached by the firm on a client's behalf. Uploads can be reassigned to a different checklist item, e.g. to correct a misfiled document. Every upload carries two provenance fields: source says who created it (portal = the client via the upload portal, api = the firm via the API or MCP), and assignment_method says how it got attached to its item (ai = the classifier, client = the client dropped it directly onto the item, firm = a firm user or API call assigned it, null = not yet assigned). Reassigning an upload overwrites assignment_method. Both fields are null on uploads that predate provenance tracking. file_url is a short-lived signed URL for the original file, and document_copy_url (set once a clean copy is ready) is the same for the derived clean-copy PDF — fetch them promptly rather than storing them; re-fetch the upload for fresh URLs.
/api/v1/requests/:request_id/uploads
List uploads
Returns all uploads for a given request. An item can hold multiple uploads (e.g. one photo per page of a multi-page document); `page_number` gives each upload's position within its item, assigned in upload order. It is `null` for uploads not yet assigned to an item.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
curl https://dointake.com/api/v1/requests/req_xyz789/uploads \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "upl_001",
"status": "processed",
"file_size": 245000,
"source": "portal",
"content_type": "application/pdf",
"created_at": "2025-01-20T14:30:00Z",
"original_filename": "w2-2025.pdf",
"ai_classification": "W-2",
"ai_confidence": 0.97,
"ai_quality_issues": [],
"ai_mismatch": null,
"submitted_ip": "203.0.113.42",
"page_number": 1,
"assignment_method": "ai",
"ai_quality_ok": true,
"document_copy_url": null,
"document_copy_status": null,
"file_url": "https://storage.googleapis.com/…signed…",
"item_id": "item_001"
}
]
}
/api/v1/uploads/:id
Get an upload
Fetches a single upload. Useful for polling `document_copy_status` after requesting a clean copy: it moves from `processing` to `ready` (with `document_copy_url` set) or `failed`.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Upload ID |
curl https://dointake.com/api/v1/uploads/upl_001 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "upl_001",
"status": "classified",
"file_size": 1245000,
"source": "portal",
"content_type": "image/jpeg",
"created_at": "2025-01-20T14:30:00Z",
"original_filename": "w2-photo.jpg",
"ai_classification": "W-2",
"ai_confidence": 0.97,
"ai_quality_issues": [
"hard_to_read"
],
"ai_mismatch": null,
"submitted_ip": "203.0.113.42",
"page_number": 1,
"assignment_method": "ai",
"ai_quality_ok": false,
"document_copy_url": "https://storage.googleapis.com/…signed…",
"document_copy_status": "ready",
"file_url": "https://storage.googleapis.com/…signed…",
"item_id": "itm_abc123"
}
}
/api/v1/uploads/:id/document_copy
Create a clean copy
Creates a "clean copy" of a photo upload: a derived scanner-style PDF, perspective-corrected and cleaned to black-and-white. The original file is never modified. Requires a Pro subscription (402 with code `upgrade_required` otherwise) and a JPEG or PNG upload (422 with `not_an_image` otherwise). Generation is asynchronous: the response is a 202 with `document_copy_status: "processing"` — poll the upload until it is `ready` (then `document_copy_url` is a download URL) or `failed`. Idempotent: re-posting while processing or ready changes nothing.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Upload ID |
curl -X POST https://dointake.com/api/v1/uploads/upl_001/document_copy \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "upl_001",
"status": "classified",
"file_size": 1245000,
"source": "portal",
"content_type": "image/jpeg",
"created_at": "2025-01-20T14:30:00Z",
"original_filename": "w2-photo.jpg",
"ai_classification": "W-2",
"ai_confidence": 0.97,
"ai_quality_issues": [
"hard_to_read"
],
"ai_mismatch": null,
"submitted_ip": "203.0.113.42",
"page_number": 1,
"assignment_method": "ai",
"ai_quality_ok": false,
"document_copy_url": null,
"document_copy_status": "processing",
"file_url": "https://storage.googleapis.com/…signed…",
"item_id": "itm_abc123"
}
}
/api/v1/uploads/:id
Assign upload to item
Assigns an upload to a checklist item on the same request. Use this to correct a misfiled upload, e.g. after classifying it yourself from a `document.uploaded` webhook. The upload's status becomes `classified` and the item becomes `received` (firing the `item.received` webhook). If the item the upload was moved away from has no uploads left, it is reset to `pending` and its review status is cleared. If the item already has uploads, the upload is appended as its next page (`page_number`). Assigning an item from a different request returns a 422 with code `cross_request_item`.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Upload ID |
item_id |
string | required | Item ID on the same request to assign the upload to |
curl -X PATCH https://dointake.com/api/v1/uploads/upl_001 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"item_id": "itm_abc123"}'
{
"data": {
"id": "upl_001",
"status": "classified",
"file_size": 245000,
"source": "portal",
"content_type": "application/pdf",
"created_at": "2025-01-20T14:30:00Z",
"original_filename": "w2-2025.pdf",
"ai_classification": "W-2",
"ai_confidence": 0.97,
"ai_quality_issues": [],
"ai_mismatch": null,
"submitted_ip": "203.0.113.42",
"page_number": 1,
"assignment_method": "firm",
"ai_quality_ok": true,
"document_copy_url": null,
"document_copy_status": null,
"file_url": "https://storage.googleapis.com/…signed…",
"item_id": "itm_abc123"
}
}
/api/v1/items/:item_id/uploads
Upload a file to an item
Uploads a document file directly onto a checklist item on the client's behalf — e.g. when a client emailed you the document instead of using the portal. Send the file as `multipart/form-data` under the `file` field. The upload is attached to the item and the item becomes `received` (firing `document.uploaded` and `item.received` webhooks), so reminders for it stop. The upload gets `source: "api"` and `assignment_method: "firm"`. AI classification runs in an advisory capacity: quality flags (`ai_quality_issues`) and a wrong-item check (`ai_mismatch: true` when the AI confidently reads the file as a different item on the request) are recorded asynchronously, but the upload is never re-filed and its status never changes. Accepted content: PDF or image (validated by file content, not extension), max 10 MB. Returns 400 with code `missing_file` if no file is sent, 422 with `invalid_file` for unaccepted content, 422 with `file_too_large` over the limit.
| Parameter | Type | Required | Description |
|---|---|---|---|
item_id |
string | required | Item ID |
file |
file | required | The document file (multipart/form-data) |
curl -X POST https://dointake.com/api/v1/items/itm_abc123/uploads \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "[email protected]"
{
"data": {
"id": "upl_002",
"status": "classified",
"file_size": 182000,
"source": "api",
"content_type": "application/pdf",
"created_at": "2025-01-21T09:15:00Z",
"original_filename": "bank-statement.pdf",
"ai_classification": null,
"ai_confidence": null,
"ai_quality_issues": [],
"ai_mismatch": null,
"submitted_ip": "198.51.100.7",
"page_number": 1,
"assignment_method": "firm",
"ai_quality_ok": null,
"document_copy_url": null,
"document_copy_status": null,
"file_url": "https://storage.googleapis.com/…signed…",
"item_id": "itm_abc123"
}
}
/api/v1/requests/:request_id/export
Export a request's files
Downloads a zip archive containing every file uploaded to the request, plus a PDF of each completed form's responses. Responds with the binary zip (`Content-Type: application/zip`), not JSON. Returns 404 if the request has no uploads or completed forms.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
curl https://dointake.com/api/v1/requests/req_xyz789/export \
-H "Authorization: Bearer YOUR_API_KEY" \
-o request-files.zip
(binary zip archive)
/api/v1/deals/:deal_id/export
Export a deal's files
Downloads a zip archive of every file uploaded across the deal's requests, plus a PDF of each completed form's responses, with one folder per request. Responds with the binary zip (`Content-Type: application/zip`), not JSON. Returns 404 if the deal has no uploads or completed forms.
| Parameter | Type | Required | Description |
|---|---|---|---|
deal_id |
string | required | Deal ID |
curl https://dointake.com/api/v1/deals/deal_abc123/export \
-H "Authorization: Bearer YOUR_API_KEY" \
-o deal-files.zip
(binary zip archive)
Form Templates
Form templates define reusable sets of fields that can be attached to requests. When attached, the template's fields are snapshotted so future edits don't affect existing requests.
/api/v1/form-templates
List form templates
Returns all form templates for your account.
curl https://dointake.com/api/v1/form-templates \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "tmpl_abc123",
"name": "Client Intake",
"description": "Basic client information form",
"updated_at": "2025-01-15T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z",
"field_count": 5
}
]
}
/api/v1/form-templates/:id
Get a form template
Retrieve a form template with all its field definitions.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Template ID |
curl https://dointake.com/api/v1/form-templates/tmpl_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "tmpl_abc123",
"name": "Client Intake",
"description": "Basic client information form",
"fields": [
{
"id": 1,
"label": "Full Legal Name",
"position": 0,
"type": "text",
"config": {
"validation": "none"
},
"description": null,
"required": true
},
{
"id": 2,
"label": "Email Address",
"position": 1,
"type": "email",
"config": {
"validation": "email"
},
"description": null,
"required": true
},
{
"id": 3,
"label": "Filing Status",
"position": 2,
"type": "select",
"config": {
"options": [
"Single",
"Married",
"Head of Household"
]
},
"description": "Select your tax filing status",
"required": true
}
],
"updated_at": "2025-01-15T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z"
}
}
/api/v1/form-templates
Create a form template
Create a new form template, optionally with fields in one call. Field types: text, textarea, number, date, email, phone, select, radio, checkbox, signature, section_heading. Fields with options (select, radio, checkbox) need config.options. Layout-only types (section_heading) render visually but have no response value — they are skipped during validation and submission.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | required | Template name |
description |
string | optional | Template description |
fields |
array | optional | List of field objects with type, label, required, description, config |
curl -X POST https://dointake.com/api/v1/form-templates \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Client Intake", "fields": [{"type": "text", "label": "Full Name", "required": true}, {"type": "email", "label": "Email", "config": {"validation": "email"}}]}'
{
"data": {
"id": "tmpl_abc123",
"name": "Client Intake",
"description": null,
"fields": [
{
"id": 1,
"label": "Full Name",
"position": 0,
"type": "text",
"config": {
"validation": "none"
},
"description": null,
"required": true
},
{
"id": 2,
"label": "Email",
"position": 1,
"type": "email",
"config": {
"validation": "email"
},
"description": null,
"required": false
}
],
"updated_at": "2025-01-15T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z"
}
}
/api/v1/form-templates/:id
Delete a form template
Delete a form template. Returns 409 if the template is attached to any requests.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Template ID |
curl -X DELETE https://dointake.com/api/v1/form-templates/tmpl_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
204 No Content
/api/v1/form-templates/:form_template_id/fields
Add a field
Add a field to an existing form template. The field is appended to the end. A field can be conditionally shown based on an earlier select/radio/checkbox field's answer via config.visibility: {"combinator": "all", "conditions": [{"field_id": "3", "operator": "equals", "value": "Yes"}]}. Use operator "equals" for select/radio parents and "contains" for checkbox parents; exactly one condition is supported. Hidden fields skip validation and are omitted from responses.
| Parameter | Type | Required | Description |
|---|---|---|---|
form_template_id |
string | required | Template ID |
type |
string | required | Field type: text, textarea, number, date, email, phone, select, radio, checkbox, signature, section_heading (layout-only, no response value) |
label |
string | required | Field label |
description |
string | optional | Help text |
required |
boolean | optional | Whether field is required (default: false) |
config |
object | optional | Field config — options for select/radio/checkbox, validation rule, visibility condition |
curl -X POST https://dointake.com/api/v1/form-templates/tmpl_abc123/fields \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "select", "label": "State", "config": {"options": ["CA", "NY", "TX"]}}'
{
"data": {
"id": 4,
"label": "State",
"position": 3,
"type": "select",
"config": {
"options": [
"CA",
"NY",
"TX"
]
},
"description": null,
"required": false
}
}
/api/v1/form-templates/:form_template_id/fields/:id
Update a field
Update a field on a form template.
| Parameter | Type | Required | Description |
|---|---|---|---|
form_template_id |
string | required | Template ID |
id |
integer | required | Field ID |
label |
string | optional | Updated label |
required |
boolean | optional | Updated required flag |
config |
object | optional | Updated config |
curl -X PATCH https://dointake.com/api/v1/form-templates/tmpl_abc123/fields/4 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label": "Updated Label", "required": true}'
{
"data": {
"id": 4,
"label": "Updated Label",
"position": 3,
"type": "select",
"config": {
"options": [
"CA",
"NY",
"TX"
]
},
"description": null,
"required": true
}
}
/api/v1/form-templates/:form_template_id/fields/:id
Delete a field
Remove a field from a form template.
| Parameter | Type | Required | Description |
|---|---|---|---|
form_template_id |
string | required | Template ID |
id |
integer | required | Field ID |
curl -X DELETE https://dointake.com/api/v1/form-templates/tmpl_abc123/fields/4 \
-H "Authorization: Bearer YOUR_API_KEY"
204 No Content
Request Forms
Attach form templates or inline forms to requests. Clients fill them out in the portal alongside document uploads.
/api/v1/requests/:request_id/forms
Attach a form
Attach a saved template or create an inline form on a request. To attach a template, send template_id. For an inline form, send name and fields.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
template_id |
string | optional | Template ID (for saved templates) |
name |
string | optional | Form name (for inline forms) |
fields |
array | optional | Field definitions (for inline forms) |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/forms \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template_id": "tmpl_abc123"}'
{
"data": {
"id": "sub_def456",
"fields": [
{
"id": "1",
"label": "Full Name",
"position": 0,
"type": "text",
"config": {},
"required": true
},
{
"id": "2",
"label": "Email",
"position": 1,
"type": "email",
"config": {
"validation": "email"
},
"required": true
}
],
"completed": false,
"completed_at": null,
"template_name": "Client Intake",
"created_at": "2025-01-20T09:00:00Z"
}
}
/api/v1/requests/:request_id/forms
List attached forms
List all forms attached to a request. Completed forms include responses.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
curl https://dointake.com/api/v1/requests/req_xyz789/forms \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "sub_def456",
"fields": [
{
"id": "1",
"label": "Full Name",
"position": 0,
"type": "text",
"config": {},
"required": true
}
],
"completed": true,
"completed_at": "2025-01-21T15:00:00Z",
"template_name": "Client Intake",
"created_at": "2025-01-20T09:00:00Z",
"responses": [
{
"label": "Full Name",
"type": "text",
"value": "Jane Smith",
"field_id": "1"
}
]
}
]
}
/api/v1/requests/:request_id/forms/:id
Detach a form
Remove an attached form from a request. Also deletes any responses.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
id |
string | required | Request form ID (sub_xxx) |
curl -X DELETE https://dointake.com/api/v1/requests/req_xyz789/forms/sub_def456 \
-H "Authorization: Bearer YOUR_API_KEY"
204 No Content
PDF Templates
Upload and manage form-fillable PDF templates. Uploaded PDFs are analyzed for native AcroForm fields. You can also place custom fields via the editor UI.
/api/v1/pdf-templates
List PDF templates
List all PDF templates for the authenticated user.
curl https://dointake.com/api/v1/pdf-templates \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "ptl_abc123",
"name": "W-9 Form",
"filename": "w9.pdf",
"file_size": 85432,
"created_at": "2025-02-01T10:00:00Z",
"page_count": 1,
"has_native_fields": true,
"custom_field_count": 0,
"native_field_count": 12
}
]
}
/api/v1/pdf-templates/:id
Get a PDF template
Get a PDF template with field definitions and an editor URL for the placement UI.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Template ID (ptl_xxx) |
curl https://dointake.com/api/v1/pdf-templates/ptl_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "ptl_abc123",
"name": "W-9 Form",
"filename": "w9.pdf",
"file_size": 85432,
"content_type": "application/pdf",
"created_at": "2025-02-01T10:00:00Z",
"native_field_definitions": [
{
"name": "topmostSubform[0].Page1[0].f1_1[0]",
"type": "Text"
}
],
"page_count": 1,
"has_native_fields": true,
"custom_field_definitions": [],
"editor_url": "https://dointake.com/dashboard/pdf-templates/ptl_abc123"
}
}
/api/v1/pdf-templates
Upload a PDF template
Upload a PDF file as a new template. Use multipart/form-data with a 'file' field. The PDF is analyzed for page count and native form fields.
| Parameter | Type | Required | Description |
|---|---|---|---|
file |
file | required | PDF file (multipart upload) |
curl -X POST https://dointake.com/api/v1/pdf-templates \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "[email protected]"
{
"data": {
"id": "ptl_def456",
"name": "w9",
"filename": "w9.pdf",
"file_size": 85432,
"content_type": "application/pdf",
"created_at": "2025-02-01T10:00:00Z",
"native_field_definitions": [
{
"name": "topmostSubform[0].Page1[0].f1_1[0]",
"type": "Text"
}
],
"page_count": 1,
"has_native_fields": true,
"custom_field_definitions": [],
"editor_url": "https://dointake.com/dashboard/pdf-templates/ptl_def456"
}
}
/api/v1/pdf-templates/:id
Delete a PDF template
Delete a PDF template. Returns 409 if the template is attached to any requests.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Template ID (ptl_xxx) |
curl -X DELETE https://dointake.com/api/v1/pdf-templates/ptl_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
204 No Content
PDF Fill Requests
Attach PDF templates to requests as fill requests. Clients fill in the PDF fields in the portal and the completed PDF is stored as an upload.
/api/v1/requests/:request_id/pdf-fills
Attach a PDF fill request
Attach a PDF template to a request. The client will be prompted to fill in the PDF fields. For documents needing multiple signatures (e.g. two spouses plus the account owner), pass an ordered `signers` list — each entry is either `{"client_id": "cl_xxx"}` or `{"owner": true}` with an optional `role_label` — and optionally `field_assignments` mapping custom field ids to a 0-based signer index. Signers sign in parallel via their own links; the final PDF (with a certificate listing every signer) is composed server-side and delivered to everyone once the last signer finishes.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
pdf_template_id |
string | required | PDF template ID (ptl_xxx) |
signers |
array | optional | Ordered signer list for multi-signature documents. Each entry: {"client_id": "cl_xxx"} or {"owner": true}, optional "role_label". |
field_assignments |
object | optional | Map of custom field id to 0-based signer index. Assigned fields are only editable by that signer. |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/pdf-fills \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pdf_template_id": "ptl_abc123",
"signers": [
{"client_id": "cl_husband1", "role_label": "Husband"},
{"client_id": "cl_wife1", "role_label": "Wife"},
{"owner": true, "role_label": "Attorney"}
],
"field_assignments": {"2": 0, "3": 1, "4": 2}
}'
{
"data": {
"id": "pfr_ghi789",
"status": "pending",
"completed_at": null,
"template_id": "ptl_abc123",
"template_name": "Service Agreement",
"created_at": "2025-02-01T12:00:00Z",
"completed_upload_id": null,
"signatures_count": 0,
"signers": [
{
"id": "pfs_aaa111",
"owner": false,
"status": "pending",
"signed_at": null,
"client_id": "cl_husband1",
"role_label": "Husband"
},
{
"id": "pfs_bbb222",
"owner": false,
"status": "pending",
"signed_at": null,
"client_id": "cl_wife1",
"role_label": "Wife"
},
{
"id": "pfs_ccc333",
"owner": true,
"status": "pending",
"signed_at": null,
"client_id": null,
"role_label": "Attorney"
}
],
"has_native_fields": false
}
}
/api/v1/requests/:request_id/pdf-fills
List PDF fill requests
List all PDF fill requests for a given request.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
curl https://dointake.com/api/v1/requests/req_xyz789/pdf-fills \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "pfr_ghi789",
"status": "completed",
"completed_at": "2025-02-02T09:30:00Z",
"template_id": "ptl_abc123",
"template_name": "W-9 Form",
"created_at": "2025-02-01T12:00:00Z",
"completed_upload_id": "upl_xyz123",
"signatures_count": 2,
"signers": [
{
"id": "pfs_aaa111",
"owner": false,
"status": "signed",
"signed_at": "2025-02-02T09:30:00Z",
"client_id": "cl_abc123",
"role_label": null
}
],
"has_native_fields": true
}
]
}
/api/v1/requests/:request_id/pdf-fills/:id
Delete a PDF fill request
Remove a PDF fill request from a request.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
id |
string | required | Fill request ID (pfr_xxx) |
curl -X DELETE https://dointake.com/api/v1/requests/req_xyz789/pdf-fills/pfr_ghi789 \
-H "Authorization: Bearer YOUR_API_KEY"
204 No Content
/api/v1/requests/:request_id/pdf-fills/:fill_id/signatures
List signature audit records
Return the audit trail for a completed fill: per-signature field name, SHA-256 hash, IP, user agent, signed-at timestamp, and (for multi-signer documents) the signer's identity. These are the same records rendered on the Certificate of Completion page appended to every signed PDF.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
fill_id |
string | required | Fill request ID (pfr_xxx) |
curl https://dointake.com/api/v1/requests/req_xyz789/pdf-fills/pfr_ghi789/signatures \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"signed_at": "2025-02-02T09:29:55Z",
"user_agent": "Mozilla/5.0",
"ip_address": "203.0.113.42",
"field_name": "borrower_signature",
"signer_id": "pfs_aaa111",
"signature_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"signer_email": "[email protected]",
"signer_name": "Jane Borrower"
}
]
}
Payments
Collect money from clients as part of a request — retainers, filing fees, invoices. The client pays through Stripe Checkout from their portal; funds go to the account owner's connected Stripe account minus a 1% platform fee. Requires a connected Stripe account with charges enabled (Settings → Billing → Client Payments). USD only for now. Amounts are integers in cents; the minimum is 100 ($1.00). A payment counts toward request completion once its status is paid. Statuses: pending, processing (async payment method in flight), paid, failed, refunded.
/api/v1/requests/:request_id/payments
Add a payment to a request
Adds a payment the client owes on this request. Returns 422 with code `payments_not_enabled` if the account has no connected Stripe account with charges enabled.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
name |
string | required | What the payment is for |
amount_cents |
integer | required | Amount in cents (USD), minimum 100 |
description |
string | optional | Optional note shown to the client |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/payments \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Retainer",
"amount_cents": 150000,
"description": "Initial retainer for Chapter 7 filing"
}'
{
"data": {
"id": "pay_abc123",
"name": "Retainer",
"status": "pending",
"description": "Initial retainer for Chapter 7 filing",
"required": true,
"currency": "usd",
"created_at": "2026-08-18T12:00:00Z",
"amount_cents": 150000,
"paid_at": null,
"refunded_at": null
}
}
/api/v1/requests/:request_id/payments
List payments
Lists all payments on a request with their statuses.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
curl https://dointake.com/api/v1/requests/req_xyz789/payments \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "pay_abc123",
"name": "Retainer",
"status": "paid",
"description": null,
"required": true,
"currency": "usd",
"created_at": "2026-08-18T12:00:00Z",
"amount_cents": 150000,
"paid_at": "2026-08-18T14:30:00Z",
"refunded_at": null
}
]
}
/api/v1/requests/:request_id/payments/:id
Remove a payment
Removes a pending or failed payment from a request. Paid or processing payments cannot be deleted (422 `not_deletable`).
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
id |
string | required | Payment ID (pay_xxx) |
curl -X DELETE https://dointake.com/api/v1/requests/req_xyz789/payments/pay_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
204 No Content
/api/v1/requests/:request_id/payments/:id/refund
Refund a payment
Fully refunds a paid payment. The client gets the entire amount back, including the platform fee. Only paid payments can be refunded (422 `not_refundable`); partial refunds are not supported yet. The client can pay again afterwards if needed.
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id |
string | required | Request ID |
id |
string | required | Payment ID (pay_xxx) |
curl -X POST https://dointake.com/api/v1/requests/req_xyz789/payments/pay_abc123/refund \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "pay_abc123",
"name": "Retainer",
"status": "refunded",
"description": null,
"required": true,
"currency": "usd",
"created_at": "2026-08-18T12:00:00Z",
"amount_cents": 150000,
"paid_at": "2026-08-18T14:30:00Z",
"refunded_at": "2026-08-19T09:00:00Z"
}
}
Deals
Deals represent multi-party transactions (e.g. real estate closings, loan applications). Each deal has parties, and each party can have associated document requests.
/api/v1/deals
List deals
Returns all deals. Optionally filter by status, external ID, or external source.
| Parameter | Type | Required | Description |
|---|---|---|---|
status |
string | optional | Filter by status (e.g. active, closed) |
external_id |
string | optional | Filter by external ID |
external_source |
string | optional | Filter by external source (used with external_id) |
assignee_id |
string | optional | Filter by workspace member (mem_…), or "unassigned" |
curl https://dointake.com/api/v1/deals \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "deal_001",
"name": "123 Main St Closing",
"status": "active",
"metadata": {},
"created_at": "2025-01-15T10:30:00Z",
"assignee": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
},
"external_id": "opp_456",
"external_source": "salesforce",
"property_address": "123 Main St, Springfield, IL 62701",
"party_count": 3
}
]
}
/api/v1/deals/:id
Get a deal
Retrieve a deal with all parties and their requests.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Deal ID |
curl https://dointake.com/api/v1/deals/deal_001 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "deal_001",
"name": "123 Main St Closing",
"status": "active",
"description": "Residential purchase",
"metadata": {},
"created_at": "2025-01-15T10:30:00Z",
"assignee": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
},
"external_id": "opp_456",
"external_source": "salesforce",
"parties": [
{
"id": "pty_001",
"position": 0,
"requests": [
{
"id": "req_xyz789",
"status": "sent",
"title": "Buyer Documents",
"item_count": 5
}
],
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"role": "Buyer",
"created_at": "2025-01-15T10:30:00Z",
"cc_emails": [],
"delegate_to_id": null,
"external_id": null,
"external_source": null
}
],
"property_address": "123 Main St, Springfield, IL 62701",
"party_count": 1
}
}
/api/v1/deals
Create a deal
Create a new deal. Optionally create from a deal template by passing `template_id`.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | required | Deal name |
description |
string | optional | Deal description |
assignee_id |
string | optional | Workspace member to assign (mem_… from GET /members). Defaults to your workspace's routing rule. |
template_id |
string | optional | Create from a deal template |
external_id |
string | optional | External system ID |
external_source |
string | optional | External system name |
metadata |
object | optional | Arbitrary key-value metadata |
property_address |
string | optional | Property address for the deal |
curl -X POST https://dointake.com/api/v1/deals \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "123 Main St Closing"}'
{
"data": {
"id": "deal_002",
"name": "123 Main St Closing",
"status": "active",
"description": null,
"metadata": {},
"created_at": "2025-01-15T10:30:00Z",
"assignee": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
},
"external_id": null,
"external_source": null,
"parties": [],
"property_address": null,
"party_count": 0
}
}
/api/v1/deals/:id
Update a deal
Update an existing deal.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Deal ID |
name |
string | optional | Deal name |
description |
string | optional | Deal description |
assignee_id |
string | optional | Reassign to a workspace member (mem_… from GET /members); empty string unassigns |
external_id |
string | optional | External system ID |
external_source |
string | optional | External system name |
metadata |
object | optional | Arbitrary key-value metadata |
property_address |
string | optional | Property address for the deal |
curl -X PATCH https://dointake.com/api/v1/deals/deal_001 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "456 Oak Ave Closing"}'
{
"data": {
"id": "deal_001",
"name": "456 Oak Ave Closing",
"status": "active",
"description": "Residential purchase",
"metadata": {},
"created_at": "2025-01-15T10:30:00Z",
"assignee": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
},
"external_id": null,
"external_source": null,
"parties": [],
"property_address": null,
"party_count": 1
}
}
/api/v1/deals/upsert
Upsert a deal
Create or update a deal by matching on `external_id` + `external_source`. Returns `201` if a new deal was created, `200` if an existing deal was updated.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | required | Deal name |
description |
string | optional | Deal description |
external_id |
string | required | External system ID (used for matching) |
external_source |
string | required | External system name (used with external_id for matching) |
metadata |
object | optional | Arbitrary key-value metadata |
property_address |
string | optional | Property address for the deal |
curl -X PUT https://dointake.com/api/v1/deals/upsert \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "123 Main St Closing", "external_id": "opp_456", "external_source": "salesforce", "property_address": "123 Main St, Springfield, IL 62701"}'
{
"data": {
"id": "deal_002",
"name": "123 Main St Closing",
"status": "active",
"description": null,
"metadata": {},
"created_at": "2025-01-15T10:30:00Z",
"assignee": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
},
"external_id": "opp_456",
"external_source": "salesforce",
"parties": [],
"property_address": "123 Main St, Springfield, IL 62701",
"party_count": 0
}
}
/api/v1/deals/dispatch
Dispatch a deal
One-call deal dispatch. Creates a deal from a template, assigns clients to parties (creating clients as needed), creates requests, and optionally sends them. Clients are matched by `external_id` + `external_source` or by `email`.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id |
string | required | Deal template ID (dtpl_xxx) |
name |
string | required | Deal name |
property_address |
string | optional | Property address |
external_id |
string | optional | External system ID for the deal |
external_source |
string | optional | External system name for the deal |
auto_send |
boolean | optional | If true, automatically send all requests to clients (default: false) |
parties |
array | required | Array of party objects with role and client info: {role, client: {email, name, external_id, external_source}} |
curl -X POST https://dointake.com/api/v1/deals/dispatch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template_id": "dtpl_abc123", "name": "Smith Purchase", "auto_send": true, "parties": [{"role": "Borrower", "client": {"email": "[email protected]", "name": "Jane Smith"}}]}'
{
"data": {
"id": "deal_xyz789",
"name": "Smith Purchase",
"status": "active",
"description": null,
"metadata": {},
"created_at": "2025-01-15T10:30:00Z",
"assignee": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
},
"external_id": null,
"external_source": null,
"parties": [
{
"id": "pty_001",
"position": 0,
"requests": [
{
"id": "req_abc789",
"status": "sent",
"title": "Borrower — Documents",
"item_count": 3
}
],
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"role": "Borrower",
"created_at": "2025-01-15T10:30:00Z",
"cc_emails": [],
"delegate_to_id": null,
"external_id": null,
"external_source": null
}
],
"property_address": null,
"party_count": 1
}
}
Deal Templates
Deal templates define reusable multi-party workflows. Each template has party roles with pre-configured form templates, PDF templates, and document checklists. Create a deal from a template to get all parties set up in one call.
/api/v1/deal-templates
List deal templates
Returns all deal templates available to the user, including system templates.
curl https://dointake.com/api/v1/deal-templates \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "dtpl_abc123",
"name": "Mortgage Loan",
"description": "Standard mortgage loan closing workflow",
"created_at": "2025-01-15T10:30:00Z",
"is_system": true,
"party_count": 10
}
]
}
/api/v1/deal-templates/:id
Get a deal template
Retrieve a deal template with all party roles and their attached form templates, PDF templates, and document types.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Deal template ID |
curl https://dointake.com/api/v1/deal-templates/dtpl_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "dtpl_abc123",
"name": "Mortgage Loan",
"description": "Standard mortgage loan closing workflow",
"created_at": "2025-01-15T10:30:00Z",
"parties": [
{
"position": 0,
"role": "Borrower",
"document_types": [
{
"id": 1,
"name": "Pay Stub"
}
],
"form_templates": [
{
"id": "ftpl_abc123",
"name": "Personal Info"
}
],
"pdf_templates": [
{
"id": "ptl_abc123",
"name": "W-9 Form"
}
]
},
{
"position": 1,
"role": "Escrow",
"document_types": [],
"form_templates": [],
"pdf_templates": []
}
],
"is_system": true,
"party_count": 2
}
}
/api/v1/deals
Create deal from template
Create a deal from a template by passing `template_id`. All party roles from the template are created automatically. Assign clients to parties afterward.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | required | Deal name |
template_id |
string | required | Deal template ID (dtpl_xxx) |
description |
string | optional | Deal description |
curl -X POST https://dointake.com/api/v1/deals \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Smith Purchase", "template_id": "dtpl_abc123"}'
{
"data": {
"id": "deal_xyz789",
"name": "Smith Purchase",
"status": "draft",
"description": null,
"metadata": {},
"created_at": "2025-01-15T10:30:00Z",
"assignee": {
"id": "mem_k2j9x",
"name": "Pat Lee",
"email": "[email protected]",
"role": "member"
},
"external_id": null,
"external_source": null,
"parties": [
{
"id": "pty_001",
"position": 0,
"requests": [],
"client": null,
"role": "Borrower",
"created_at": "2025-01-15T10:30:00Z",
"cc_emails": [],
"delegate_to_id": null,
"external_id": null,
"external_source": null
},
{
"id": "pty_002",
"position": 1,
"requests": [],
"client": null,
"role": "Escrow",
"created_at": "2025-01-15T10:30:00Z",
"cc_emails": [],
"delegate_to_id": null,
"external_id": null,
"external_source": null
}
],
"property_address": null,
"party_count": 2
}
}
/api/v1/deals/:deal_id/parties/:party_id/requests/from-template
Create request from template
Create a fully-loaded request for a party using the deal template's attachments. Automatically attaches all form templates, PDF templates, and document checklists defined for this party's role. The party must have a client assigned.
| Parameter | Type | Required | Description |
|---|---|---|---|
deal_id |
string | required | Deal ID |
party_id |
string | required | Party ID |
curl -X POST https://dointake.com/api/v1/deals/deal_xyz789/parties/pty_001/requests/from-template \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "req_abc789",
"status": "draft",
"title": "Borrower — Documents",
"items": [
{
"id": "item_001",
"name": "Pay Stub",
"status": "pending"
}
],
"client": {
"id": "cli_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"created_at": "2025-01-15T10:30:00Z"
}
}
Parties
Parties represent participants in a deal (e.g. buyer, seller, lender). Each party can be linked to a client.
/api/v1/deals/:deal_id/parties
List parties
Returns all parties for a deal.
| Parameter | Type | Required | Description |
|---|---|---|---|
deal_id |
string | required | Deal ID |
curl https://dointake.com/api/v1/deals/deal_001/parties \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "pty_001",
"position": 0,
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"role": "Buyer",
"created_at": "2025-01-15T10:30:00Z",
"cc_emails": [
"[email protected]"
],
"delegate_to_id": null,
"external_id": null,
"external_source": null
}
]
}
/api/v1/deals/:deal_id/parties
Create a party
Add a party to a deal. Optionally link to an existing client.
| Parameter | Type | Required | Description |
|---|---|---|---|
deal_id |
string | required | Deal ID |
role |
string | required | Party role (e.g. Buyer, Seller) |
client_id |
string | optional | Link to an existing client |
cc_emails |
array of strings | optional | CC email addresses for notifications (max 10) |
delegate_to_id |
string | optional | Party ID to delegate requests to. The delegate's client receives emails instead. Must be in the same deal. Set to null to clear. |
external_id |
string | optional | External system ID for this party |
external_source |
string | optional | External system name (e.g. follow_up_boss) |
curl -X POST https://dointake.com/api/v1/deals/deal_001/parties \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"role": "Seller", "client_id": "cl_def456", "cc_emails": ["[email protected]"]}'
{
"data": {
"id": "pty_002",
"position": 1,
"client": {
"id": "cl_def456",
"name": "Bob Jones",
"email": "[email protected]"
},
"role": "Seller",
"created_at": "2025-01-15T10:30:00Z",
"cc_emails": [
"[email protected]"
],
"delegate_to_id": null,
"external_id": null,
"external_source": null
}
}
/api/v1/parties/:id
Update a party
Update a party's role or linked client.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Party ID |
role |
string | optional | Party role |
client_id |
string | optional | Link to a different client |
cc_emails |
array of strings | optional | CC email addresses for notifications (max 10) |
delegate_to_id |
string | optional | Party ID to delegate requests to. The delegate's client receives emails instead. Must be in the same deal. Set to null to clear. |
external_id |
string | optional | External system ID for this party |
external_source |
string | optional | External system name (e.g. follow_up_boss) |
curl -X PATCH https://dointake.com/api/v1/parties/pty_001 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"delegate_to_id": "pty_002"}'
{
"data": {
"id": "pty_001",
"position": 0,
"client": {
"id": "cl_abc123",
"name": "Jane Smith",
"email": "[email protected]"
},
"role": "Primary Buyer",
"created_at": "2025-01-15T10:30:00Z",
"cc_emails": [
"[email protected]",
"[email protected]"
],
"delegate_to_id": "pty_002",
"external_id": null,
"external_source": null
}
}
Activity Log
An immutable audit trail of events in your account. Use this endpoint to track changes to deals, requests, parties, and documents.
/api/v1/activity
List activity
Returns activity log entries for your account, newest first. Supports cursor-based pagination and filtering.
| Parameter | Type | Required | Description |
|---|---|---|---|
deal_id |
string | optional | Filter by deal public ID |
party_id |
string | optional | Filter by party public ID |
request_id |
string | optional | Filter by request public ID |
resource_type |
string | optional | Filter by resource type (deal, request, party, item, upload) |
event_type |
string | optional | Filter by event type (e.g. request.sent, deal.created) |
before |
string | optional | Cursor for pagination — pass an activity ID to fetch older entries |
limit |
integer | optional | Max entries to return (1–100, default 50) |
curl https://dointake.com/api/v1/activity?deal_id=deal_abc123 \
-H "Authorization: Bearer sk_live_..."
{
"data": [
{
"id": "act_x1y2z3a4b5",
"metadata": {
"client_name": "Jane Doe",
"request_title": "Tax Documents 2025"
},
"request_id": "req_abc123",
"deal_id": "deal_abc123",
"event_type": "request.sent",
"resource_id": "req_abc123",
"resource_type": "request",
"party_id": "pty_def456",
"occurred_at": "2025-03-15T10:30:00Z"
}
]
}
Webhooks
Subscribe to events in your account. When an event occurs, we send an HTTP POST to your configured URL with a JSON payload.
/api/v1/webhooks
List webhooks
Returns all webhook subscriptions.
curl https://dointake.com/api/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"active": true,
"id": "wh_001",
"events": [
"request.completed",
"document.uploaded"
],
"description": "Production webhook",
"url": "https://example.com/webhook",
"created_at": "2025-01-15T10:30:00Z"
}
]
}
/api/v1/webhooks
Create a webhook
Create a new webhook subscription. The response includes a `secret` for verifying webhook signatures — store it securely, it won't be shown again.
| Parameter | Type | Required | Description |
|---|---|---|---|
url |
string | required | URL to receive webhook events |
events |
array | required | Event types to subscribe to |
description |
string | optional | Description for this webhook |
active |
boolean | optional | Whether the webhook is active (default: true) |
curl -X POST https://dointake.com/api/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/webhook", "events": ["request.completed"]}'
{
"data": {
"active": true,
"id": "wh_002",
"events": [
"request.completed"
],
"description": null,
"secret": "whsec_abc123...",
"url": "https://example.com/webhook",
"created_at": "2025-01-15T10:30:00Z"
}
}
/api/v1/webhooks/:id
Update a webhook
Update a webhook subscription.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Webhook ID |
url |
string | optional | URL to receive webhook events |
events |
array | optional | Event types to subscribe to |
description |
string | optional | Description |
active |
boolean | optional | Whether the webhook is active |
curl -X PATCH https://dointake.com/api/v1/webhooks/wh_001 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"active": false}'
{
"data": {
"active": false,
"id": "wh_001",
"events": [
"request.completed",
"document.uploaded"
],
"description": "Production webhook",
"url": "https://example.com/webhook",
"created_at": "2025-01-15T10:30:00Z"
}
}
/api/v1/webhooks/:id
Delete a webhook
Permanently delete a webhook subscription.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | required | Webhook ID |
curl -X DELETE https://dointake.com/api/v1/webhooks/wh_001 \
-H "Authorization: Bearer YOUR_API_KEY"
# 204 No Content
Webhook Events
When an event occurs, we send a POST request to your webhook URL with a JSON payload.
Available event types:
| Event | Fired when |
|---|---|
request.sent | A request is sent to the client |
request.completed | A request is marked complete |
request.reopened | A completed or canceled request is reopened (portal link live again) |
request.canceled | A request is canceled (portal closed, reminders stopped) |
request.overdue | A request passes its due date |
request.never_viewed | 3 days after sending with no client portal view (once per request; the professional is emailed too) |
reminder.sent | The reminder scheduler sends (or, if delegated, would have sent) a client reminder |
portal.renewal_requested | A client hits an expired portal link and asks for a fresh one |
document.uploaded | A file is uploaded — by the client, or by you via the API |
document.classified | An upload is auto-classified |
item.created | A checklist item is added |
item.received | An item receives an upload |
item.re_requested | An upload is rejected and the item is sent back to the client |
all_items_received | Every item on a request has been received |
pdf_form.completed | A client completes a fillable PDF form |
pdf_form.signer_signed | One signer of a multi-signer PDF signs |
payment.paid | A client's payment is confirmed |
payment.failed | A client's async payment fails (they can retry) |
payment.refunded | A payment is refunded in full |
client.created / client.updated | A client is created / updated |
deal.created / deal.completed / deal.status_changed | Deal lifecycle |
party.created / party.assigned | Deal party lifecycle |
email.bounced | An email to a client hard-bounced (address is dead; reminders stop) |
email.complained | A client marked an Intake email as spam (treated like a bounce) |
ping | Test event, sent from the webhook settings page |
Envelope:
{
"id": "dlv_9k2m4p71xw",
"event": "reminder.sent",
"mode": "live",
"data": { ... },
"created_at": "2026-08-05T17:20:00Z"
}
id is stable across delivery retries of the same event — dedupe on it. (It is also sent as the X-Webhook-Delivery header.) Two reminders are two events with two ids; a retry of one reminder reuses the same id.
The mode field is "test" for events from sandbox data, "live" for production data. Use this to filter or route test events in your automation platform.
Request-scoped payloads. Events about a request (request.*, reminder.sent, item.re_requested, portal.renewal_requested, all_items_received) nest shared blocks under data:
{
"request": {
"id": "req_a1b2c3",
"title": "Chapter 7 — Intake Documents",
"status": "in_progress",
"due_date": "2026-08-20",
"sent_at": "2026-08-01T15:04:00Z",
"completed_at": null,
"locale": "en",
"assignee": { "id": "mem_k2j9x", "role": "member", "name": "Pat Lee", "email": "[email protected]" },
"portal_url": "https://dointake.com/p/XXXXXXXX",
"portal_url_expires_at": null
},
"client": {
"id": "cli_d4e5f6",
"name": "Maria Alvarez",
"email": "[email protected]",
"phone": "+15551234567",
"external_id": "CASE-2026-081",
"external_source": null,
"tags": [],
"notification_preference": "email",
"sms_consent": null
},
"delivery": { "intake_email_sent": true }
}
portal_url is the live client portal link (the same link the email carries) — treat webhook endpoint URLs as secrets and use HTTPS, since payloads contain live portal tokens. portal_url_expires_at is null while the request is active and set to completion + 90 days once it completes.
delivery.intake_email_sent says whether Intake also emailed the client for this event. A 200 from your endpoint means your automation received the event — not that the client was reached; anything you send downstream is your responsibility.
email.bounced / email.complained carry the same request and client blocks plus an email block (email_id, subject, and for bounces a bounce object with type/subType/message) — no delivery block, since these events are about a failed email, not a notification you might deliver.
Upload provenance. Upload payloads (document.uploaded, document.classified, and the upload block on item.received) include source and assignment_method. source says who created the upload: "portal" (the client) or "api" (your integration, e.g. an on-behalf-of upload). assignment_method says how the upload got attached to its item: "ai" (the classifier), "client" (the client dropped it directly onto the item, bypassing classification — ai_classification will be null), "firm" (a firm user, API call, or MCP tool assigned it), or null (not yet assigned — always the case on document.uploaded for portal uploads that haven't been classified yet; document.classified and item.received carry the final value). Both fields are null on uploads that predate provenance tracking.
Reading document.classified. The upload's status tells you whether the classifier actually filed it. "classified" means it was attached to an item; "needs_review" means the classifier had a guess but wasn't confident enough to file it (ai_classification still carries that guess), so it is waiting on a human or on your POST /uploads/:id/assign call. An upload with status: "classified" is always attached to an item.
request.never_viewed adds days_since_sent. reminder.sent adds a reminder block: number (this reminder's count), cap (the request's max_reminders, null when unlimited), final (true when the cap has been reached), days_since_last_contact, and pending_items — ordered as the portal displays them, each with id, type (form | document | pdf), name, description, required. item.re_requested adds an item block including the professional's reason note.
Delivery retries: 3 attempts per delivery — 60s after the first failure, 5 minutes after the second, with a 10-second response timeout. Any non-2xx response or timeout counts as a failure. After 10 consecutive failures across deliveries, the endpoint is disabled and must be re-enabled in settings.
Notification delegation. In Settings → Webhooks you can delegate delivery of request.sent, reminder.sent, item.re_requested, portal.renewal_requested, and request.completed to one of your endpoints. For clients whose notification_preference is external, Intake then stops emailing them for that event and your automation delivers it instead (delivery.intake_email_sent will be false). Clients preferring email keep getting Intake emails regardless. If the delegated endpoint is deleted, disabled, or auto-disabled after repeated failures, the delegation lapses: Intake email resumes immediately and you're alerted — clients are never left without notifications.
Webhook Security
Every webhook delivery includes an X-Intake-Signature header in the form:
X-Intake-Signature: t=1712345678,v1=5257a869e7...
t is the Unix timestamp (seconds) when the delivery was signed, and v1 is a hex-encoded HMAC-SHA256 signature of the string "{t}.{raw_request_body}", signed with your webhook secret.
To verify a delivery:
- Split the header on
,and extracttandv1. - Concatenate the timestamp, a literal
., and the raw request body (before any JSON parsing). - Compute HMAC-SHA256 of that string with your webhook secret and hex-encode it.
- Compare it to
v1using a constant-time comparison. - Optionally reject deliveries whose timestamp is too old (e.g. more than 5 minutes) to guard against replay.
Always verify signatures to ensure payloads are from Intake and haven't been tampered with.
Verification — Node.js
Verify in Node.js
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('='))
);
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected)
);
}
Verification — Python
Verify in Python
import hmac, hashlib
def verify_webhook(raw_body, signature_header, secret):
parts = dict(p.split("=", 1) for p in signature_header.split(","))
signed_payload = f"{parts['t']}.{raw_body}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(parts["v1"], expected)
Verification — Ruby
Verify in Ruby
require 'openssl'
def verify_webhook(raw_body, signature_header, secret)
parts = signature_header.split(',').to_h { |p| p.split('=', 2) }
expected = OpenSSL::HMAC.hexdigest(
'sha256', secret, "#{parts['t']}.#{raw_body}"
)
Rack::Utils.secure_compare(parts['v1'], expected)
end
Verification — PHP
Verify in PHP
function verifyWebhook($rawBody, $signatureHeader, $secret) {
$parts = [];
foreach (explode(',', $signatureHeader) as $pair) {
[$k, $v] = explode('=', $pair, 2);
$parts[$k] = $v;
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
MCP Protocol
Intake exposes a native Model Context Protocol (MCP) server, letting AI agents like Claude, GPT, or custom agents manage your entire intake workflow programmatically.
Endpoint: POST https://dointake.com/mcp
Protocol: JSON-RPC 2.0 (MCP spec 2025-03-26)
Authentication: OAuth 2.1 (recommended) or API key Bearer token. Tool access respects read / write scopes.
Connecting as a Claude Connector (Recommended)
Intake is available as a native Claude Connector. This is the easiest way to connect — no API key or config files needed.
In Claude Desktop or claude.ai:
- Open Settings → Connectors
- Click Add Connector
- Enter the URL:
https://dointake.com/mcp - Claude will automatically discover the authorization server, open a consent screen, and connect to your Intake account
That's it. Claude handles authorization and token management automatically.
Connecting with an API Key
For programmatic agents, CI/CD, or clients that don't support OAuth, you can use an API key instead.
Claude Desktop — Add to your claude_desktop_config.json:
{
"mcpServers": {
"intake": {
"url": "https://dointake.com/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
Cursor — Add to your .cursor/mcp.json:
{
"mcpServers": {
"intake": {
"url": "https://dointake.com/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
Protocol Flow
A typical MCP session follows three steps:
1. Initialize — Negotiate protocol version and discover server capabilities.
→ {"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-03-26",
"clientInfo": {"name": "my-agent", "version": "1.0"}}}
← {"jsonrpc": "2.0", "id": 1, "result": {
"protocolVersion": "2025-03-26",
"capabilities": {"tools": {"listChanged": false}},
"serverInfo": {"name": "intake", "version": "1.0.0", "mode": "live"}}}
The mode field in serverInfo reflects the credential used to connect — "test" for test API keys or test OAuth clients, "live" for production credentials.
2. List tools — Discover available tools and their input schemas.
→ {"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
← {"jsonrpc": "2.0", "id": 2, "result": {"tools": [...]}}
3. Call a tool — Execute a tool with arguments.
→ {"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "list_clients", "arguments": {}}}
← {"jsonrpc": "2.0", "id": 3, "result": {
"content": [{"type": "text", "text": "[{...}]"}]}}
Tool results are returned as JSON text in the content array. Errors set isError: true:
← {"jsonrpc": "2.0", "id": 4, "result": {
"content": [{"type": "text", "text": "Request not found"}],
"isError": true}}
Example: Full Workflow
An agent can handle a complete intake flow without any dashboard interaction:
1. create_client(name, email)
2. create_request(client_id, title, items, co_clients)
3. attach_inline_form(request_id, name, fields) — or attach_form_to_request
4. upload_pdf_template(file_base64, filename) — optional
5. attach_pdf_to_request(request_id, template_id) — optional
6. send_request(request_id)
7. get_request(id) → poll for completion
8. get_form_responses(request_id) → read submitted data
tools/call → list_clients
list_clients
List all clients
tools/call → create_client
create_client
Create a new client
tools/call → update_client
update_client
Update a client's name, email, phone, notification preference, or SMS consent
tools/call → list_members
list_members
List workspace members who can be assigned work
tools/call → list_requests
list_requests
List document requests (filter by status, client, deal, assignee)
tools/call → get_request
get_request
Get request with items, uploads, and portal URLs for every recipient
tools/call → create_request
create_request
Create a request with checklist items and co-clients
tools/call → update_request
update_request
Update title, description, due date, assignee, reminders, max_reminders, notify_professional_after_days
tools/call → assign_request
assign_request
Assign a request to a workspace member, or unassign it
tools/call → add_checklist_item
add_checklist_item
Add a document item to a request
tools/call → update_item
update_item
Set review status and note on an item
tools/call → re_request_item
re_request_item
Re-request a rejected item
tools/call → send_request
send_request
Send a draft request to the client (supports skip_email); errors if the request has nothing for the client to do
tools/call → complete_request
complete_request
Mark a sent request as complete
tools/call → cancel_request
cancel_request
Cancel a request — closes the client portal and stops reminders, no email sent
tools/call → list_request_reminders
list_request_reminders
List reminder audit trail for a request
tools/call → list_uploads
list_uploads
List uploads for a request
tools/call → assign_upload
assign_upload
Assign an upload to a checklist item (correct misfiles)
tools/call → upload_to_item
upload_to_item
Upload a file (base64) directly onto a checklist item on the client's behalf
tools/call → create_document_copy
create_document_copy
Create a clean scanner-style PDF copy of a photo upload (Pro, async)
tools/call → list_form_templates
list_form_templates
List form templates
tools/call → get_form_template
get_form_template
Get template with fields
tools/call → create_form_template
create_form_template
Create template with fields
tools/call → add_form_field
add_form_field
Add a field to a form template
tools/call → attach_form_to_request
attach_form_to_request
Attach a form template to a request
tools/call → attach_inline_form
attach_inline_form
Create and attach a one-off form
tools/call → get_form_responses
get_form_responses
Get submitted form responses
tools/call → list_pdf_templates
list_pdf_templates
List PDF templates
tools/call → get_pdf_template
get_pdf_template
Get template with field info
tools/call → upload_pdf_template
upload_pdf_template
Upload a PDF (base64-encoded)
tools/call → attach_pdf_to_request
attach_pdf_to_request
Attach a PDF to a request as a fill request
tools/call → add_payment_to_request
add_payment_to_request
Add a payment the client owes on a request
tools/call → list_request_payments
list_request_payments
List the payments on a request
tools/call → refund_payment
refund_payment
Fully refund a paid payment
tools/call → get_pdf_fill_signatures
get_pdf_fill_signatures
Return the signature audit trail (field, hash, IP, UA, signed_at) for a completed fill
Sandbox Mode
Test your integrations without affecting production data. Same API, same endpoints, same MCP tools — just use a test key.
How It Works
- Create a test API key in Settings → API Keys (select "Test" mode)
- Use the test key in your integration — same
Authorization: Bearerheader, same endpoints - Toggle to Test mode in the dashboard to view sandbox data and captured emails
- When ready, swap to a live key — no code changes needed
What's Different in Sandbox
| Feature | Live Mode | Test Mode |
|---|---|---|
| Data | Production data | Isolated sandbox data |
| Emails | Sent to real recipients | Captured in Sandbox Inbox |
| Plan limits | Enforced | Bypassed (unlimited test data) |
| Webhooks | Delivered with "mode": "live" | Delivered with "mode": "test" |
| File uploads | Normal | Normal |
| Portal links | Normal | Shows "test request" banner |
| MCP tools | Production data | Sandbox data |
OAuth Clients (MCP)
When registering an OAuth client via dynamic registration (POST /oauth/register), include "mode": "test" to create a test-mode client. All tokens issued by that client will access sandbox data only.
POST /oauth/register
{
"client_name": "My Test Agent",
"redirect_uris": ["https://example.com/callback"],
"mode": "test"
}
Sandbox Inbox
In test mode, a Sandbox Inbox appears in the dashboard sidebar. Emails that would normally be sent to clients are captured here instead, showing the exact HTML that would have been delivered. Click any email to preview it, including working portal links.
Make.com Integration
Connect Intake to 1,000+ apps using Make.com (formerly Integromat). Here's a step-by-step guide:
1. Create a webhook in Make.com
Create a new scenario and add a "Webhooks" module → "Custom webhook". Copy the webhook URL.
2. Register the webhook in Intake
Use the API to create a webhook pointing to your Make.com URL:
POST /api/v1/webhooks
{"url": "https://hook.make.com/your-id", "events": ["request.completed"]}
3. Map the data
When Make.com receives its first webhook, it will learn the data structure. Use the fields to trigger actions in other apps — create a row in Google Sheets, send a Slack notification, update a CRM, etc.
4. Common recipes
- Request completed → Slack notification — Get notified when a client finishes uploading documents
- Upload created → Google Drive — Automatically save uploaded files to a specific folder
- Deal created → CRM update — Sync new deals to your CRM system
Testing with Sandbox
Before going live, test your Make.com scenario with sandbox data:
- Create a test API key in Settings → API Keys
- Use the test key when registering your webhook:
Authorization: Bearer dk_test_... - Trigger test events — Make.com will receive webhooks with
"mode": "test"in the payload - Validate your scenario works end-to-end
- Swap to your live API key to go live
Zapier Integration
Connect Intake to 6,000+ apps using Zapier. Use webhooks to trigger Zaps when events occur in Intake.
1. Create a Catch Hook in Zapier
Create a new Zap and choose Webhooks by Zapier → Catch Hook as the trigger. Copy the webhook URL.
2. Register the webhook in Intake
POST /api/v1/webhooks
Authorization: Bearer YOUR_API_KEY
{"url": "https://hooks.zapier.com/hooks/catch/your-id", "events": ["request.completed", "document.uploaded"]}
3. Test and map fields
Trigger a test event (send a document request to a test client), then return to Zapier to map the incoming fields to your action step.
Testing with Sandbox
- Create a test API key in Settings → API Keys
- Register your webhook using the test key
- Create and send test requests — Zapier receives webhooks with
"mode": "test" - Build and validate your Zap with sandbox data
- When ready, create a new webhook with your live key to go live
Twin.so / MCP Client Setup
Connect any MCP-compatible AI agent (Twin.so, Claude Desktop, custom agents) to Intake.
Quick Start with API Key
Configure your MCP client with the Intake server URL and an API key:
{
"url": "https://dointake.com/mcp",
"headers": {"Authorization": "Bearer YOUR_API_KEY"}
}
OAuth 2.1 (Recommended for Production)
For production integrations, use OAuth 2.1 with dynamic client registration:
- Register your client:
POST /oauth/register - Direct user to authorize:
GET /oauth/authorize - Exchange code for token:
POST /oauth/token - Use the token:
Authorization: Bearer ACCESS_TOKEN
See the MCP Protocol section for the full protocol flow.
Testing with Sandbox
- Create a test API key or register an OAuth client with
"mode": "test" - Connect your MCP client with test credentials
- The initialize response will show
"mode": "test"inserverInfo - All tool calls create and access sandbox data
- Emails are captured in the Sandbox Inbox — check the dashboard
- When satisfied, swap to live credentials — same tools, real data
Zoho One Integration
Connect Intake to Zoho One using Zoho Flow or Deluge scripts.
Option 1: Zoho Flow (No Code)
- In Zoho Flow, create a new flow with a Webhook trigger
- Copy the webhook URL
- Register it in Intake:
POST /api/v1/webhooks
Authorization: Bearer YOUR_API_KEY
{"url": "https://flow.zoho.com/your-webhook-url", "events": ["request.completed"]}
- Map incoming webhook fields to Zoho CRM, Zoho Books, or other Zoho apps
Option 2: Deluge Scripts (Custom Logic)
Call the Intake API directly from Deluge scripts in Zoho CRM, Creator, or other Zoho apps:
// Deluge script example: List clients
headers = Map();
headers.put("Authorization", "Bearer YOUR_API_KEY");
response = invokeurl
[
url: "https://dointake.com/api/v1/clients"
type: GET
headers: headers
];
Testing with Sandbox
- Create a test API key in Intake (Settings → API Keys, mode: Test)
- Use
dk_test_...key in your Zoho Flow webhooks or Deluge scripts - Test your entire workflow with sandbox data
- Swap to your
dk_live_...key when ready to go live