Skip to content

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

bash
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

bash
curl "https://api.ondayzero.com/api/v1/notes" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
json
{
  "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

bash
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

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

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

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

json
{
  "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_at is set. A none reminder becomes inactive. A recurring reminder moves scheduled_at forward 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_at is 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.

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

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

bash
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

bash
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

bash
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 a 404 (NOT_FOUND_099, "Todo not found.").
  • PATCH /api/v1/todos/{todo_id} accepts title, description, due_date, reminder_at, priority, entity_type and entity_id (not business_id). As with notes, null means "not sent", so these fields cannot be cleared through the API; an empty body is a 400 (GEN_002). Sending reminder_at re-arms the reminder (reminder_sent goes back to false), 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

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