Client Collaboration
When a business is managed by an advisory firm, three route families let the firm and the client work in the same system instead of over email:
/api/v1/client-messages— threaded conversations the client starts and the firm answers./api/v1/document-requests— the firm asks for a file, the client uploads it./api/v1/transaction-clarifications— the firm attaches a question (or a receipt request) to a transaction, the client answers.
Each family has two halves:
| Half | Who | Paths | Business header |
|---|---|---|---|
| Client | People with access to the business (client-portal users, business members) | …/conversations, …/document-requests, …/transaction-clarifications |
x-business-id required |
| Firm | Staff of the advisory firm that manages the business | paths containing /firm/ |
none; pass firm_id and business_id as query parameters where noted |
All requests need Authorization: Bearer dz_your_token_here. Successful responses are wrapped as {"success": true, "data": …}. Dates are YYYY-MM-DD and money is integer cents. Firm-side calls take no x-business-id except where stated.
Permissions: what read-only users can and cannot do
Client-portal users hold the firm role client, which grants the business read permission only. Direct business viewers are read-only too. Each route needs one permission, taken from its HTTP method (GET = read, POST and PUT = write) unless the route declares otherwise. Three client routes declare read on purpose so a view-only client can still answer:
| Client call | Permission | Read-only client user |
|---|---|---|
| List/read conversations, unread count | read |
Yes |
| Start a conversation, reply to a conversation | write |
No, 403 AUTH_012 ("Permission required: 'write'") |
| List document requests, open count, download a file | read |
Yes |
Upload a requested document (…/fulfill) |
read (declared) |
Yes |
| List clarifications, open count | read |
Yes |
| Answer a clarification, attach a receipt | read (declared) |
Yes |
Other limits that apply to everyone:
- A token with the
read_onlyscope may only sendGET,HEADandOPTIONS. Uploading, answering and replying are allPOSTs, so they return403AUTH_048for such a token whatever the user's role (see Authentication). - Business-scoped
POSTs are refused with403BILLING_001when the business has neither an active trial nor an active subscription, including a client's upload or answer. Reads still work. - Client calls only ever see the business named in
x-business-id. Another business's id returns404. - Firm calls need a staff role in the firm (
owner,adminormember). A firmclientorviewergets403. Firmmembers are limited to the businesses assigned to them: out-of-scope businesses return403when you filter bybusiness_idand404when you fetch a record by id.
Pagination and filters
Every list takes limit (1–200, default 50), cursor (the next_cursor of the previous page) and include_total_count (default false; total is null unless you ask). See Pagination.
- Conversation lists return
items,total,limit,next_cursor,prev_cursor,has_nextandhas_prev, newest activity first. - Document request and clarification lists return
items,total,has_nextandnext_cursor, newest first.
A status (or category) filter is a plain string match: a value that does not exist returns an empty list, not an error.
Client messages
A conversation has a subject, a category, a status and an ordered list of messages. Only the client side starts conversations; the firm replies and sets the status.
category |
Use |
|---|---|
general (default) |
Anything else |
document_request |
Discussion about a needed file |
tax_question |
Taxes |
payment_question |
Invoices, bills, payments |
other |
Catch-all |
status |
Meaning |
|---|---|
open |
Active (the state of a new conversation) |
waiting_on_client |
The firm is waiting for the client |
resolved |
Closed |
Replies do not change the status; the firm sets it. The API does not block a client reply to a resolved conversation (the portal simply hides the box).
Messaging must be switched on for the firm. Starting a conversation or replying while it is off returns 403 AUTH_043; reading still works. A firm admin turns it on with client_messaging_enabled on PUT /api/v1/advisory-firms/{firm_id} or the Client Messaging switch on the Your Firm page. A business with no managing firm cannot start a conversation (400 VALIDATION_170).
Client routes
# Start a conversation
curl -X POST "https://api.ondayzero.com/api/v1/client-messages/conversations" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"subject": "Question about Q4 tax filing",
"category": "tax_question",
"message": "Can you confirm our Q4 estimated payment went out?"
}'subject is 1–500 characters, message 1–10,000, category defaults to general. The response data is the conversation:
{
"id": "019c0000-…", "business_id": "…", "advisory_firm_id": "…",
"subject": "Question about Q4 tax filing", "category": "tax_question", "status": "open",
"created_by_user_id": "…", "created_by_name": "Pat Client", "business_name": "Acme LLC",
"messages": [
{"id": "…", "conversation_id": "019c0000-…", "sender_user_id": "…", "sender_role": "client",
"sender_name": "Pat Client", "body": "Can you confirm our Q4 estimated payment went out?",
"read_at": null, "created_at": "2026-10-11T14:02:11Z"}
],
"created_at": "2026-10-11T14:02:11Z", "updated_at": "2026-10-11T14:02:11Z"
}| Method and path | What it does |
|---|---|
GET /api/v1/client-messages/conversations |
List the business's conversations; filters status, category |
POST /api/v1/client-messages/conversations |
Start one (body above) |
GET /api/v1/client-messages/conversations/{conversation_id} |
Full thread. Marks the firm's messages read |
POST /api/v1/client-messages/conversations/{conversation_id}/messages |
Reply with {"body": "…"} (1–10,000 characters); returns the full conversation |
GET /api/v1/client-messages/unread-count |
{"unread_conversations": n}: conversations with unread firm messages |
List items are summaries: id, business_id, advisory_firm_id, subject, category, status, created_by_user_id, created_by_name, business_name, message_count, last_message_preview (first 120 characters of the latest message), last_message_at, unread_count, created_at, updated_at. Every client of the business sees all of its conversations, not only their own. A conversation id from another business is 404 NOT_FOUND_036.
Firm routes
Pass firm_id as a query parameter; it defaults to the first firm where you are staff.
| Method and path | What it does |
|---|---|
GET /api/v1/client-messages/firm/conversations |
All conversations across the firm's businesses; filters business_id, status, category |
GET /api/v1/client-messages/firm/conversations/{conversation_id} |
Full thread. Marks the client's messages read |
POST /api/v1/client-messages/firm/conversations/{conversation_id}/messages |
Reply with {"body": "…"} |
PUT /api/v1/client-messages/firm/conversations/{conversation_id}/status |
Set the status with {"status": "resolved"} (open, waiting_on_client or resolved) |
GET /api/v1/client-messages/firm/unread-count |
Conversations with unread client messages |
curl -X PUT "https://api.ondayzero.com/api/v1/client-messages/firm/conversations/CONVERSATION_ID/status?firm_id=YOUR_FIRM_ID" \
-H "Authorization: Bearer dz_your_token_here" \
-H "Content-Type: application/json" \
-d '{"status": "waiting_on_client"}'A conversation outside the firm or your business scope is 404 NOT_FOUND_036; replying or changing the status as a non-member of the firm is 403 AUTH_044. There is no firm route to start a conversation. A new conversation emails the firm's members and posts to the firm's Slack webhook; an advisor reply emails the person who started the conversation (both respect notification preferences).
Document requests
The firm asks for a named document; the client uploads it; either side can download it.
status |
Meaning |
|---|---|
open |
Waiting for the client's upload |
fulfilled |
The client uploaded the file |
cancelled |
The firm withdrew the request |
A request has title (1–255 characters), optional description (up to 2,000) and optional due_date. The API does not mark requests overdue; compare due_date with today yourself. Response fields: id, business_id, advisory_firm_id, requested_by_user_id, requested_by_name, title, description, status, due_date, file_name, has_file, fulfilled_at, business_name, created_at, updated_at.
Firm routes
Create a request for a client business (the business must be actively managed by your firm and in your scope; otherwise 403 BUSINESS_003):
curl -X POST "https://api.ondayzero.com/api/v1/document-requests/firm/businesses/BUSINESS_ID" \
-H "Authorization: Bearer dz_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"title": "January 2026 bank statement",
"description": "PDF export from your bank for the full month.",
"due_date": "2026-02-15"
}'The client users of that business get a "Document requested" notification.
| Method and path | What it does |
|---|---|
GET /api/v1/document-requests/firm |
List across the firm; filters firm_id, business_id, status |
GET /api/v1/document-requests/firm/count |
{"open_count": n} for the firm |
GET /api/v1/document-requests/firm/{request_id}/download |
Download link for the uploaded file |
POST /api/v1/document-requests/firm/{request_id}/cancel |
Set the request to cancelled (no body). The API does not check its current status first |
Client routes
| Method and path | What it does |
|---|---|
GET /api/v1/document-requests |
The business's requests; filter status (open, fulfilled, cancelled) |
GET /api/v1/document-requests/count |
{"open_count": n} |
POST /api/v1/document-requests/{request_id}/fulfill |
Upload the file (multipart) |
GET /api/v1/document-requests/{request_id}/download |
Download link for the file |
Upload with a multipart/form-data field named file:
curl -X POST "https://api.ondayzero.com/api/v1/document-requests/REQUEST_ID/fulfill" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-F "file=@january-statement.pdf"The request becomes fulfilled, file_name and fulfilled_at are set, the person who made the request gets a "Document uploaded" notification, and a document_request.fulfilled webhook event is sent once per request. The response is the updated request. Upload rules are the shared ones in Uploads: a supported extension whose leading bytes match it (400 VALIDATION_052 or VALIDATION_195), at most 250 MB (400 VALIDATION_054; an over-large request body is a 413), and a malware scan. Other errors: 404 NOT_FOUND_038 (unknown request or another business's), 400 BUSINESS_003 if the request is not open.
Both download routes return a short-lived link, valid for 15 minutes:
{"success": true, "data": {"download_url": "https://…", "file_name": "january-statement.pdf"}}They return 404 NOT_FOUND_038 when no file has been uploaded.
Transaction clarifications
A clarification is a question about one transaction. The firm asks, the client answers in words, optionally with a receipt, and the firm resolves it.
status |
Meaning |
|---|---|
open |
Waiting for the client |
answered |
The client replied |
resolved |
The firm closed it; no further answers or receipts |
request_type is question (default) or receipt. A receipt request marks the transaction as requested from the client; uploading a file answers it. Response fields: id, business_id, transaction_id, advisory_firm_id, question, options, status, request_type, answer, asked_by_user_id, asked_by_name, answered_at, resolved_at, created_at, updated_at, plus transaction context (transaction_description, transaction_counterparty, transaction_amount in cents, transaction_date, business_name) and attachments (id, filename, uploaded_at for files the client sent).
Firm routes
curl -X POST "https://api.ondayzero.com/api/v1/transaction-clarifications/firm/transactions/TRANSACTION_ID?business_id=BUSINESS_ID" \
-H "Authorization: Bearer dz_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"question": "Was this a business meal or personal? If business, who attended?",
"options": ["Business meal", "Personal — exclude"],
"request_type": "question"
}'question is 1–2,000 characters; options are optional suggested answers (one-tap replies in the portal). The business comes from business_id or, if omitted, the x-business-id header; with neither you get a 400. The firm is resolved from the business: 403 BUSINESS_003 if no firm manages it or you are not on that firm's staff or scope, 404 if the transaction is not in the business.
| Method and path | What it does |
|---|---|
POST /api/v1/transaction-clarifications/firm/transactions/{transaction_id} |
Ask (above). A receipt request also notifies the client |
GET /api/v1/transaction-clarifications/firm |
List across the firm; filters firm_id, business_id, status |
GET /api/v1/transaction-clarifications/firm/count |
{"open_count": n}: clarifications still open |
POST /api/v1/transaction-clarifications/firm/{clarification_id}/resolve |
Close it. Send an empty JSON body {}; add ?firm_id= if you belong to several firms |
To request receipts for many transactions at once, use POST /api/v1/transactions/batch/receipt-requests with x-business-id, transaction_ids (1–200) and an optional note (up to 500 characters). It creates one receipt clarification per transaction that needs one, skips the rest, and returns requested, already_requested, already_attached, exempt, not_found and requested_transaction_ids. The client gets a single notification, and unanswered requests are reminded according to the business's receipt policy. See Transactions.
Client routes
| Method and path | What it does |
|---|---|
GET /api/v1/transaction-clarifications |
The business's questions; filter status (open, answered, resolved) |
GET /api/v1/transaction-clarifications/count |
{"open_count": n} |
POST /api/v1/transaction-clarifications/{clarification_id}/answer |
Answer with {"answer": "…"} (1–4,000 characters) |
POST /api/v1/transaction-clarifications/{clarification_id}/receipt |
Attach a receipt (multipart field file) |
curl -X POST "https://api.ondayzero.com/api/v1/transaction-clarifications/CLARIFICATION_ID/answer" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"answer": "Business meal with the Acme team."}'Answering sets status to answered, records answer and answered_at, and sends a transaction_clarification.answered webhook event (once per clarification, so a receipt added after a typed answer does not announce it again). You can answer again until the firm resolves it; a resolved clarification returns 403 BUSINESS_003. An unknown id is 404 NOT_FOUND_037.
curl -X POST "https://api.ondayzero.com/api/v1/transaction-clarifications/CLARIFICATION_ID/receipt" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-F "file=@receipt.jpg"Receipts accept pdf, png, jpg, jpeg, webp, heic and heif only (400 VALIDATION_052 otherwise), plus the shared upload checks above. The file is stored on the transaction itself as a client-sourced attachment, where the firm reviews it, and it also appears in the clarification's attachments. For an open receipt request the upload counts as the answer (answer is set to "Receipt attached." and the status becomes answered). The bookkeeper who asked is notified. Receipts are allowed while the clarification is open or answered and refused (403) once it is resolved.
MCP tools for firm staff
Regent and MCP clients signed in as firm staff can use these tools (see MCP Server); a connection bound to one business sees only that business.
| Tool | Does |
|---|---|
create_document_request |
Asks the client for a file (title, optional description, optional due_date) |
list_document_requests |
Lists requests; poll status=fulfilled for new uploads |
create_transaction_clarification |
Asks about one transaction (transaction_id, question up to 2,000 characters, optional options) |
list_transaction_clarifications |
Reads questions and answers; status is open, answered or resolved |
list_firm_conversations |
Lists message threads; filter by status, category, client_business_id |
send_firm_message, update_conversation_status |
Reply to or set the status of a thread; both ask for approval first |
Resolving a clarification has no MCP tool; use the REST route above.
Error summary
Errors use the shared envelope in Error Handling.
| Status | code |
When |
|---|---|---|
400 |
GEN_002 |
Body or parameter failed validation (empty subject, over-long text, bad status, no business for a firm clarification) |
400 |
VALIDATION_170 |
The business has no managing advisory firm |
400 |
BUSINESS_003 |
Fulfilling a document request that is not open |
403 |
AUTH_012 |
The caller's role lacks the permission the route needs (see the table above) |
403 |
AUTH_043 |
Client messaging is switched off for the firm |
403 |
AUTH_044 |
Replying to or updating a conversation without being a firm member |
403 |
AUTH_048 |
The token's scope does not allow the method |
403 |
BUSINESS_003 |
Firm action on a business the firm does not manage or you cannot access; answering or attaching to a resolved clarification |
404 |
NOT_FOUND_036 / 037 / 038 |
Conversation / clarification / document request not found, or outside your business or scope |
Not covered
AI-drafted replies, the portal's unread badges, the firm's Slack and email wording, and the Settings pages that create requests are product features described in the Knowledge Base, not part of the REST surface. Messages carry no file attachments; use document requests or clarification receipts for files.