Tasks and notes
A task is a to-do for your team, such as "Call back Ada on Friday". It can be linked to a contact and assigned to a teammate. A note is free text saved on one contact. The notes field on a contact is a separate text that the Notes tab doesn’t show. To put a note where your team will see it, use Create a note . Tasks and notes belong to the account, so every seat on it sees the same ones. A rep on a team that sees only assigned contacts sees only tasks and notes on contacts assigned to them or to no one, and tasks with no contact that are assigned to them or to no one.
A task sends nothing by itself. When an open task’s due time passes, it can start an automation that uses the A task is due trigger, and that automation’s steps can text or notify someone. The due-window rules are under due_date .
Creating a task on a contact and completing it each add a line to the contact’s Activity. A saved note shows on the contact’s timeline as itself; it adds no Activity line.
# The Task object
idinteger The task id.
titlestring Up to 300 characters.
descriptionstring | null Up to 5000 characters.
statusstring pending or completed.
prioritystring low, normal, high or urgent.
due_dateISO 8601 string | null When it’s due, in UTC.
completed_atISO 8601 string | null When it was marked done. Cleared when it’s reopened.
contact_idinteger | null The linked contact. Becomes null if the contact is deleted.
first_name, last_name, phonestring | null The linked contact’s name and phone, or null with no contact.
assigned_tointeger | null The teammate’s user id.
assignee_first, assignee_laststring | null The assignee’s name.
user_idinteger The account the task belongs to.
created_atISO 8601 string When it was created.
# List tasks
One page of your tasks, soonest due first by default, with counts for all your tasks. Any seat on the account can read tasks.
Parameters
contact_idinteger · query Only this contact’s tasks.
statusstring · query pending or completed.
prioritystring · query low, normal, high or urgent.
assigned_tointeger · query Only tasks assigned to this user id.
due_before, due_afterstring · query Due at or before, or at or after, this UTC date or time.
sortstring · query due_date (the default, tasks with no due date last), priority (urgent first), newest or oldest.
limitinteger · query Default 50, max 1000.
cursorstring · query The next_cursor of the previous page. There’s no offset.
Returns a list of Task objects in data, and next_cursor (null on the last page). counts holds total, pending, completed and overdue for all the tasks you can see, whatever the filters. Each count is a string of digits.
Errors
HTTP Code When
400 invalid_fieldcontact_id or assigned_to isn’t an id, or due_before or due_after isn’t a date.
400 An offset other than 0, or a cursor that isn’t one.
500 The read failed. Retry.
curl "https://api.tryready.com/tasks?status=pending&limit=20" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/tasks?status=pending&limit=20', {
headers: {
'X-API-Key': process.env.READY_API_KEY,
},
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.get(
"https://api.tryready.com/tasks",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
params={
"status": "pending",
"limit": "20"
},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')
Returns:
{
"success": true,
"data": [
{
"id": 812,
"contact_id": 101,
"user_id": 7,
"title": "Call back about the quote",
"description": "She asked for a call after 2pm.",
"due_date": "2026-11-14T19:00:00.000Z",
"status": "pending",
"priority": "high",
"assigned_to": 15,
"completed_at": null,
"created_at": "2026-11-11T16:42:10.311Z",
"first_name": "Ada",
"last_name": "Lovelace",
"phone": "+12125550199",
"assignee_first": "Sam",
"assignee_last": "Ortiz"
}
],
"counts": {
"total": "34",
"pending": "12",
"completed": "22",
"overdue": "3"
},
"next_cursor": null
}
# Get a task
One task. Any seat on the account can read tasks.
Parameters
idinteger · path Required The task id.
Returns the Task object in data.
Errors
HTTP Code When
400 invalid_fieldThe id isn’t a whole number.
404 No task with this id on your account.
500 The read failed. Retry.
curl "https://api.tryready.com/tasks/812" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/tasks/812', {
headers: {
'X-API-Key': process.env.READY_API_KEY,
},
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.get(
"https://api.tryready.com/tasks/812",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')
# Create a task
Adds a task, open (pending). Needs the edit_contacts permission.
Parameters
titlestring Required Up to 300 characters.
More parameters
contact_idinteger A contact on your account to link it to.
due_datestring A date or date and time, such as 2026-11-14T19:00:00Z. A time with a time zone offset, like 2026-11-14T14:00:00-05:00, is converted to UTC. A time with no Z or offset is read as UTC, and a date alone is due at midnight UTC. Ready checks every minute for tasks that came due in the last 15 minutes, so a task created with a due time further in the past never starts an A task is due automation. If Ready is down for more than 15 minutes, tasks that came due during that time do not start their automation either. Moving the due time later lets it start again.
prioritystring low, normal (the default), high or urgent.
assigned_tointeger A teammate’s user id.
descriptionstring Up to 5000 characters.
Idempotency-Keystring · header A retry with the same key returns the first answer instead of a second task.
Returns 201 with the new Task object in data.
Errors
HTTP Code When
400 invalid_fieldA field is the wrong type or too long, or priority isn’t one of the four.
400 No title; a contact_id or assigned_to that isn’t on your account; or a due_date that isn’t a real date.
curl -X POST "https://api.tryready.com/tasks" \
-H "X-API-Key: $READY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Call back about the quote",
"contact_id": 101,
"due_date": "2026-11-14T19:00:00Z",
"priority": "high",
"assigned_to": 15
}'
const res = await fetch('https://api.tryready.com/tasks', {
method: 'POST',
headers: {
'X-API-Key': process.env.READY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"title": "Call back about the quote",
"contact_id": 101,
"due_date": "2026-11-14T19:00:00Z",
"priority": "high",
"assigned_to": 15
}),
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.post(
"https://api.tryready.com/tasks",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
json={
"title": "Call back about the quote",
"contact_id": 101,
"due_date": "2026-11-14T19:00:00Z",
"priority": "high",
"assigned_to": 15
},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')
# Update a task
Changes only the fields you send. Send null for contact_id, assigned_to, due_date or description to clear it. Needs the edit_contacts permission.
Parameters
idinteger · path Required The task id.
statusstring completed marks it done; pending reopens it and clears completed_at.
title, description, due_date, priority, contact_id, assigned_toas in create Same rules as Create a task .
Returns the updated Task object in data.
Errors
HTTP Code When
400 invalid_fieldThe id isn’t a whole number, or a field is the wrong type or too long.
400 A blank title or priority; a status other than the two; a contact_id or assigned_to that isn’t on your account; or a value that can’t be saved, such as a bad due_date.
404 No task with this id on your account.
409 The status changed while you were saving. The other fields were saved; read the task again.
500 task_status_failedYour other changes were saved, but the task wasn’t marked done or reopened. Try again.
curl -X PUT "https://api.tryready.com/tasks/812" \
-H "X-API-Key: $READY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"due_date": "2026-11-15T15:00:00Z",
"priority": "urgent"
}'
const res = await fetch('https://api.tryready.com/tasks/812', {
method: 'PUT',
headers: {
'X-API-Key': process.env.READY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"due_date": "2026-11-15T15:00:00Z",
"priority": "urgent"
}),
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.put(
"https://api.tryready.com/tasks/812",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
json={
"due_date": "2026-11-15T15:00:00Z",
"priority": "urgent"
},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')
# Complete a task
Marks the task done. A task that’s already done answers success again and isn’t logged twice. Send no body. Needs the edit_contacts permission.
Parameters
idinteger · path Required The task id.
Returns { "success": true }. Read the task to see completed_at.
Errors
HTTP Code When
400 invalid_fieldThe id isn’t a whole number.
400 unknown_fieldYou sent a body field.
404 No task with this id on your account.
409 The task was reopened or deleted while you were saving.
500 task_complete_failedThe save failed. Retry.
curl -X POST "https://api.tryready.com/tasks/812/complete" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/tasks/812/complete', {
method: 'POST',
headers: {
'X-API-Key': process.env.READY_API_KEY,
},
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.post(
"https://api.tryready.com/tasks/812/complete",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')
# Delete a task
Permanently removes the task. Needs the edit_contacts permission.
Parameters
idinteger · path Required The task id.
Returns { "success": true }.
Errors
HTTP Code When
400 invalid_fieldThe id isn’t a whole number.
400 unknown_fieldYou sent a body field.
404 No task with this id, or already deleted.
500 The delete failed. Retry.
curl -X DELETE "https://api.tryready.com/tasks/812" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/tasks/812', {
method: 'DELETE',
headers: {
'X-API-Key': process.env.READY_API_KEY,
},
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.delete(
"https://api.tryready.com/tasks/812",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')
# The Note object
idinteger The note id.
contact_idinteger The contact it’s on.
bodystring The text, up to 20000 characters.
user_idinteger The teammate who wrote it.
author_first, author_last, author_emailstring | null The writer’s name and email. Only in List notes .
created_at, updated_atISO 8601 string When it was written and last edited.
# List notes
Every note on one contact, newest first, in one list. Any seat on the account can read notes.
Parameters
contact_idinteger · query Required The contact.
Returns a list of Note objects in data.
Errors
HTTP Code When
400 invalid_fieldcontact_id isn’t an id.
400 contact_id_requiredNo contact_id. Send the id of the contact whose notes you want.
403 CONTACT_NOT_ASSIGNEDYou see only your assigned contacts, and this one is someone else’s.
404 CONTACT_NOT_FOUNDNo contact with this id on your account.
503 NOTES_READ_FAILEDThe read failed. Retry.
curl "https://api.tryready.com/notes?contact_id=101" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/notes?contact_id=101', {
headers: {
'X-API-Key': process.env.READY_API_KEY,
},
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.get(
"https://api.tryready.com/notes",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
params={
"contact_id": "101"
},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')
Returns:
{
"success": true,
"data": [
{
"id": 3301,
"contact_id": 101,
"user_id": 15,
"body": "Prefers texts after 5pm. Renewal is in March.",
"created_at": "2026-11-11T17:05:44.120Z",
"updated_at": "2026-11-11T17:05:44.120Z",
"author_first": "Sam",
"author_last": "Ortiz",
"author_email": "sam@example.com"
}
]
}
# Create a note
Saves a note on a contact, written by you. The same text from you on the same contact within 5 seconds isn’t saved twice: you get 200 with the first note and "deduped": true. Needs the edit_contacts permission.
Parameters
contact_idinteger Required A contact on your account.
bodystring Required The text, not blank, up to 20000 characters.
Idempotency-Keystring · header A retry with the same key returns the first answer instead of a second note.
Returns 201 with the new Note object in data.
Errors
HTTP Code When
400 invalid_fieldbody is missing, blank or too long, or contact_id isn’t an id.
400 No contact_id.
403 CONTACT_NOT_ASSIGNEDYou see only your assigned contacts, and this one is someone else’s.
404 CONTACT_NOT_FOUNDNo contact with this id on your account.
500 The note wasn’t saved. Retry.
503 NOTE_READ_FAILEDNothing was saved. Retry.
curl -X POST "https://api.tryready.com/notes" \
-H "X-API-Key: $READY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contact_id": 101,
"body": "Prefers texts after 5pm. Renewal is in March."
}'
const res = await fetch('https://api.tryready.com/notes', {
method: 'POST',
headers: {
'X-API-Key': process.env.READY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"contact_id": 101,
"body": "Prefers texts after 5pm. Renewal is in March."
}),
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.post(
"https://api.tryready.com/notes",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
json={
"contact_id": 101,
"body": "Prefers texts after 5pm. Renewal is in March."
},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')
Returns:
{
"success": true,
"data": {
"id": 3301,
"contact_id": 101,
"user_id": 15,
"body": "Prefers texts after 5pm. Renewal is in March.",
"created_at": "2026-11-11T17:05:44.120Z",
"updated_at": "2026-11-11T17:05:44.120Z"
}
}
# Update a note
Replaces the note’s text. Any seat on the account can edit a teammate’s note; user_id stays the writer. Refused while a legal hold covers your account or the contact. Needs the edit_contacts permission.
Parameters
idinteger · path Required The note id.
bodystring Required The new text, not blank, up to 20000 characters.
Returns the updated Note object in data.
Errors
HTTP Code When
400 invalid_fieldThe id isn’t a whole number, or body is missing, blank or too long.
403 CONTACT_NOT_ASSIGNEDYou see only your assigned contacts, and the note’s contact is someone else’s.
404 NOTE_NOT_FOUNDNo note with this id on your account.
423 LITIGATION_HOLDUnder a legal hold. Nothing was changed.
500 The save failed. Retry.
503 NOTE_READ_FAILEDNothing was changed. Retry.
curl -X PUT "https://api.tryready.com/notes/3301" \
-H "X-API-Key: $READY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"body": "Prefers texts after 5pm. Renewal moved to April."
}'
const res = await fetch('https://api.tryready.com/notes/3301', {
method: 'PUT',
headers: {
'X-API-Key': process.env.READY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"body": "Prefers texts after 5pm. Renewal moved to April."
}),
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.put(
"https://api.tryready.com/notes/3301",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
json={
"body": "Prefers texts after 5pm. Renewal moved to April."
},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')
# Delete a note
Permanently removes the note, whoever wrote it. Refused while a legal hold covers your account or the contact. Needs the edit_contacts permission.
Parameters
idinteger · path Required The note id.
Returns { "success": true }.
Errors
HTTP Code When
400 invalid_fieldThe id isn’t a whole number.
403 CONTACT_NOT_ASSIGNEDYou see only your assigned contacts, and the note’s contact is someone else’s.
404 NOTE_NOT_FOUNDNo note with this id, or already deleted.
423 LITIGATION_HOLDUnder a legal hold. Nothing was deleted.
500 The delete failed. Retry.
503 NOTE_READ_FAILEDNothing was deleted. Retry.
curl -X DELETE "https://api.tryready.com/notes/3301" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/notes/3301', {
method: 'DELETE',
headers: {
'X-API-Key': process.env.READY_API_KEY,
},
});
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success === false) {
throw new Error(`${res.status}${json.error_code || json.code ? ' ' + (json.error_code || json.code) : ''}: ${json.error || json.message || res.statusText}`);
}
import os, requests
r = requests.delete(
"https://api.tryready.com/notes/3301",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
timeout=30,
)
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
if not r.ok or data.get("success") is False:
raise RuntimeError(f'{r.status_code} {data.get("error_code") or data.get("code") or ""}: {data.get("error") or data.get("message") or r.reason}')