Notes and Todos
Two small families of endpoints cover free-form text and task tracking:
- Notes (
/api/v1/notes) are shared memos that belong to a business. Every user with access to the business sees the same notes. Notes support pinning, file attachments, an AI rewrite preview, an immediate team notification, and scheduled or recurring reminders. This is the data behind the Notes page in the app. - Todos (
/api/v1/todos) are personal tasks owned by the user the token belongs to. They carry a due date, a priority, an optional reminder and an optional link to another record. This is the data behind the Todos page in the app.
Both families use the standard bearer token and x-business-id header (see Authentication). Neither is a core accounting object: nothing here posts to the ledger.
Not covered. Comment threads on records (
/threads) are an internal route family that is not part of the public API, so this guide does not document them. Notes have no link to other records (see Notes versus todos); only todos can reference a record.
Permissions and gating
| Method | Permission | Notes |
|---|---|---|
GET |
read | A read_only token can call these |
POST, PATCH |
write | A read_only token gets 403 (AUTH_048) |
DELETE |
delete | Deletes are soft deletes |
Write calls also pass the billing gate: when a business's trial has ended and it has no active subscription, POST, PATCH and DELETE return 403 (BILLING_001, or BILLING_002 when no billing record exists) while GET keeps working. Notes and todos need no add-on.
All responses use the standard envelope: {"success": true, "data": {...}} for records, and {"success": true, "message": "...", "code": "..."} for deletes. Errors use the shape described in Error Handling.
Notes
Note fields
| Field | Type | Notes |
|---|---|---|
id |
UUID | |
title |
string | Up to 500 characters; defaults to Untitled Note on create |
body |
string or null | Free text, no length limit in the schema |
pinned |
boolean | Pinned notes sort first |
author_id |
UUID | The user who created the note |
author_name, author_email |
string or null | Author display details |
created_at, updated_at |
datetime | |
attachments |
array | See Attachments |
Create a note
curl -X POST "https://api.ondayzero.com/api/v1/notes" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"title": "June close checklist", "body": "Reconcile operating account.\nReview accruals."}'Both fields are optional. Unknown fields are ignored. The response data is the note, with pinned: false and an empty attachments array.
POST /api/v1/notes/bulk takes {"notes": [{"title": "...", "body": "..."}]} (up to 200 entries). It exists to migrate notes that were stored in a browser. Its response is the business's entire note list (items and total) after the import, not just the notes you sent, so it is rarely the right call for an integration. Use POST /api/v1/notes in a loop instead.
List and fetch notes
curl "https://api.ondayzero.com/api/v1/notes" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"{
"success": true,
"data": {
"items": [
{
"id": "01912345-abcd-7000-8000-000000000301",
"title": "June close checklist",
"body": "Reconcile operating account.\nReview accruals.",
"pinned": true,
"author_id": "01912345-abcd-7000-8000-000000000010",
"author_name": "Dana Rivera",
"author_email": "dana@example.com",
"created_at": "2026-06-01T14:03:00Z",
"updated_at": "2026-06-02T09:12:00Z",
"attachments": []
}
],
"total": 1
}
}The list is not paginated: it returns every note in the business, pinned notes first and then by most recent updated_at. There are no filters. GET /api/v1/notes/{note_id} returns a single note (404 if it does not exist in this business).
Update a note
curl -X PATCH "https://api.ondayzero.com/api/v1/notes/NOTE_ID" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"pinned": true}'Send any of title, body and pinned. Fields you omit are unchanged. null values are treated as "not sent", so you cannot clear a body by sending null; send an empty string instead. An empty request body is a 400 (GEN_002). Any user with access to the business can update a note, not only its author.
Delete a note
DELETE /api/v1/notes/{note_id} soft-deletes the note and returns "Note deleted successfully." (DELETE_040). A second delete, or a note from another business, is a 404.
Notify the team now
curl -X POST "https://api.ondayzero.com/api/v1/notes/NOTE_ID/notify" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Sends an inbox notification titled New note: <title> (body truncated to 500 characters) to every user of the business, including the caller, with a link back to the Notes page. The response is {"notified": <count>}. A note with an empty or whitespace-only body is rejected with 400 (VALIDATION_208). See Anomalies and Notifications for how inbox notifications are read.
AI rewrite
POST /api/v1/notes/{note_id}/rewrite (no body) asks the business's configured AI model to clean up the note body and returns a preview:
{"success": true, "data": {"rewritten_body": "Reconcile the operating account, then review accruals."}}The note is not changed. To apply the rewrite, PATCH the note with the new body. A note with an empty body returns 400 (GEN_002). If the AI service fails, the call still succeeds with 200 and rewritten_body set to an empty string, so treat an empty string as "try again later" and never write it back.
Reminders and recurrence
A reminder notifies the rest of the business on a schedule, optionally repeating.
curl -X POST "https://api.ondayzero.com/api/v1/notes/NOTE_ID/reminders" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"scheduled_at": "2026-07-01T09:00:00Z", "recurrence": "weekly"}'| Field | Type | Notes |
|---|---|---|
scheduled_at |
datetime, required | ISO 8601. Send UTC with a Z suffix; the time zone offset is discarded and the clock time is stored as UTC |
recurrence |
string | none (default), daily, weekly or monthly |
The response is the reminder:
{
"success": true,
"data": {
"id": "01912345-abcd-7000-8000-000000000401",
"note_id": "01912345-abcd-7000-8000-000000000301",
"scheduled_at": "2026-07-01T09:00:00",
"recurrence": "weekly",
"is_active": true,
"last_sent_at": null,
"created_at": "2026-06-20T15:30:00"
}
}How reminders fire:
- A background job sends due reminders. Delivery is not instant at
scheduled_at; expect a short delay. - Each firing creates an inbox notification titled
Reminder: <title>for every user of the business except the user who created the reminder. - After firing,
last_sent_atis set. Anonereminder becomes inactive. A recurring reminder movesscheduled_atforward by 1 day (daily), 7 days (weekly) or 30 days (monthly, a fixed interval, not a calendar month) and stays active. - If the note was deleted, the reminder deactivates without notifying anyone.
scheduled_atis not checked against the clock. A time in the past fires at the next job run.
GET /api/v1/notes/{note_id}/reminders returns {"items": [...]} with the note's active reminders, soonest first. DELETE /api/v1/notes/reminders/{reminder_id} cancels a reminder (it sets is_active to false) and returns a delete message. It uses the reminder's own id and takes no note id. An unknown reminder is a 404.
Attachments
Attachments are uploaded straight to storage and then registered on the note, in three steps. Allowed extensions: pdf, png, jpg, jpeg, gif, webp, csv, xls, xlsx, doc, docx, txt, md, markdown. Maximum size is 25 MB (26,214,400 bytes). The MIME type must start with image/, application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument., application/msword, text/csv, text/plain or text/markdown.
1. Request an upload URL.
curl -X POST "https://api.ondayzero.com/api/v1/notes/NOTE_ID/attachments/upload-url" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"file_name": "bank-statement.pdf", "mime_type": "application/pdf", "file_size": 482113}'file_name (up to 255 characters), mime_type (up to 100) and file_size (bytes, greater than 0) are all required. The response gives a presigned POST:
{
"success": true,
"data": {
"upload_url": "https://...",
"s3_key": "business-notes/BUSINESS_ID/NOTE_ID/0191...-bank-statement.pdf",
"method": "POST",
"fields": {"key": "...", "policy": "..."},
"max_bytes": 26214400
}
}2. Upload the file. Send one multipart POST to upload_url with every entry of fields as a form field, in the order given, followed by the file as the last field named file. Do not add the bearer token to this request. Storage rejects bodies larger than max_bytes.
3. Register the attachment.
curl -X POST "https://api.ondayzero.com/api/v1/notes/NOTE_ID/attachments" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"file_name": "bank-statement.pdf", "mime_type": "application/pdf", "file_size": 482113, "s3_key": "S3_KEY_FROM_STEP_1"}'The s3_key must be the one returned in step 1 for this same note, or the call is a 400. DayZero also inspects the uploaded file's contents and rejects a file whose real type does not match its extension. The response is an attachment: id, note_id, file_name, file_size, mime_type, uploaded_by, created_at. Attachments also appear in the note's attachments array.
GET /api/v1/notes/{note_id}/attachments/{attachment_id}/download-url returns {"download_url": "https://..."}, a short-lived presigned link. DELETE /api/v1/notes/{note_id}/attachments/{attachment_id} soft-deletes the attachment. Once a note is deleted, its attachments can no longer be reached through the API.
Todos
A todo belongs to one user and, normally, one business. Only its owner can see or change it: other users in the business get 404 for your todos, and the list returns only the caller's.
Todo fields
| Field | Type | Notes |
|---|---|---|
id |
UUID | |
user_id |
UUID | The owner |
business_id |
UUID or null | null for a personal todo that has no business |
title |
string | 1 to 255 characters |
description |
string or null | Up to 5000 characters |
completed |
boolean | |
completed_at |
datetime or null | Set when completed, cleared when reopened |
due_date |
datetime or null | |
reminder_at |
datetime or null | When to notify the owner |
reminder_sent |
boolean | true once the reminder has fired |
priority |
string | low, normal (default), high or urgent |
source |
string | manual for todos created through the API; Regent (the in-app AI assistant) creates its own with ai_chat |
entity_type, entity_id |
string or null | Optional link to a record (see below) |
status |
string | Derived: completed, overdue (not completed and due_date has passed) or pending |
created_at, updated_at |
datetime |
Datetimes you send with an offset are converted to UTC. status is computed at read time, so a todo turns overdue without any write.
Create a todo
curl -X POST "https://api.ondayzero.com/api/v1/todos" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"title": "Chase Acme invoice",
"description": "Second reminder, call their AP team.",
"business_id": "YOUR_BUSINESS_ID",
"due_date": "2026-07-03T17:00:00Z",
"reminder_at": "2026-07-03T09:00:00Z",
"priority": "high",
"entity_type": "invoice",
"entity_id": "INVOICE_UUID"
}'Only title is required. Todo request bodies reject unknown fields. business_id, when sent, must equal the x-business-id header or the call is a 403 (AUTH_010, "No access to this business."). Important: the list endpoint only returns todos whose business_id matches the current business, so a todo created without business_id is stored as personal and will not appear in GET /api/v1/todos, although you can still fetch it by id. Send business_id for any todo you want to list.
List todos
curl "https://api.ondayzero.com/api/v1/todos?completed=false&limit=50" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"| Parameter | Notes |
|---|---|
completed |
true for completed todos, false for open ones; omit for both |
limit |
1 to 1000. If you omit it, no page cap is applied and every matching todo is returned, so always pass it |
cursor, direction, sort_by, descending, include_total_count |
Standard cursor pagination, see Pagination. Default sort is created_at descending |
The response is the standard cursor page (items, next_cursor, prev_cursor, has_next, has_prev, limit, total). There is no filter by priority, due date or linked record; filter client-side.
Get, update, delete
GET /api/v1/todos/{todo_id}returns one todo. An id that is not a UUID is rejected by validation; an unknown or someone else's todo is a404(NOT_FOUND_099, "Todo not found.").PATCH /api/v1/todos/{todo_id}acceptstitle,description,due_date,reminder_at,priority,entity_typeandentity_id(notbusiness_id). As with notes,nullmeans "not sent", so these fields cannot be cleared through the API; an empty body is a400(GEN_002). Sendingreminder_atre-arms the reminder (reminder_sentgoes back tofalse), so the owner is notified again at the new time.DELETE /api/v1/todos/{todo_id}soft-deletes the todo and returns"Todo deleted successfully."(DELETE_049).
Complete and reopen
curl -X POST "https://api.ondayzero.com/api/v1/todos/TODO_ID/complete" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"POST /api/v1/todos/{todo_id}/complete sets completed: true and completed_at, and POST /api/v1/todos/{todo_id}/uncomplete reverses it. Both take no body, return the updated todo, and are safe to repeat: completing a completed todo changes nothing.
Todo reminders
When reminder_at passes on an open todo whose reminder has not been sent, a background job sends the owner an inbox notification titled Reminder: <title> (body is the description, truncated to 500 characters, or the title) with a View Todos link, then sets reminder_sent to true. A todo that is completed or deleted before the reminder time is never notified. A reminder is sent once; change reminder_at to schedule another.
Linking a todo to a record
entity_type (up to 50 characters) and entity_id (up to 36 characters) are free-form reference fields, for example invoice plus the invoice's UUID. DayZero stores them and returns them as sent. It does not check that the type is known or that the record exists, and it does not enforce any relationship, so a deleted record leaves the todo's link dangling. Use them as a pointer your integration or UI can resolve.
Notes versus todos
| Notes | Todos | |
|---|---|---|
| Visibility | Everyone with access to the business | Only the owner |
| Reminder recipients | Everyone in the business except the creator | The owner |
| Recurrence | daily, weekly, monthly |
None |
| Link to a record | None | entity_type and entity_id |
| Attachments | Yes | No |
| Pagination | None, returns all | Cursor |
| Completion state | None | completed, status |
Quick reference
| Method and path | Purpose |
|---|---|
GET /api/v1/notes, POST /api/v1/notes |
List, create notes |
POST /api/v1/notes/bulk |
Bulk-create (migration helper) |
GET, PATCH, DELETE /api/v1/notes/{note_id} |
Read, update, delete a note |
POST /api/v1/notes/{note_id}/notify |
Notify the business now |
POST /api/v1/notes/{note_id}/rewrite |
AI rewrite preview |
GET, POST /api/v1/notes/{note_id}/reminders |
List, schedule reminders |
DELETE /api/v1/notes/reminders/{reminder_id} |
Cancel a reminder |
POST /api/v1/notes/{note_id}/attachments/upload-url |
Presigned upload |
POST /api/v1/notes/{note_id}/attachments |
Register an uploaded file |
GET /api/v1/notes/{note_id}/attachments/{attachment_id}/download-url |
Download link |
DELETE /api/v1/notes/{note_id}/attachments/{attachment_id} |
Delete an attachment |
GET, POST /api/v1/todos |
List, create todos |
GET, PATCH, DELETE /api/v1/todos/{todo_id} |
Read, update, delete a todo |
POST /api/v1/todos/{todo_id}/complete, .../uncomplete |
Complete, reopen |
Send an Idempotency-Key header on POST calls you might retry (see Error Handling); a retried POST /api/v1/notes or POST /api/v1/todos without one creates a second record.