Skip to content

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_only scope may only send GET, HEAD and OPTIONS. Uploading, answering and replying are all POSTs, so they return 403 AUTH_048 for such a token whatever the user's role (see Authentication).
  • Business-scoped POSTs are refused with 403 BILLING_001 when 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 returns 404.
  • Firm calls need a staff role in the firm (owner, admin or member). A firm client or viewer gets 403. Firm members are limited to the businesses assigned to them: out-of-scope businesses return 403 when you filter by business_id and 404 when 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_next and has_prev, newest activity first.
  • Document request and clarification lists return items, total, has_next and next_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

bash
# 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:

json
{
  "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
bash
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):

bash
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:

bash
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:

json
{"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

bash
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)
bash
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.

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