Templates
A template is a message you save once and send again. These are the same templates you see under Campaigns → Templates in the app. Every list shows only your account’s own templates.
Reading needs only a key. Creating, editing, deleting and counting a use need the send_sms permission. A test key can read templates but not change them: a write answers 403 test_key_not_allowed .
Saving a template doesn’t send anything. Ready checks the text with the same content policy it uses for a send, and a blocked save counts against your account the way a blocked send does.
# The Template object
List templates returns this object. Ignore fields you don’t recognize.
idinteger The template id.
user_idinteger The account that owns the template.
namestring The template name. Names don’t have to be unique.
bodystring The message text, as saved. Merge fields such as {{contact.first_name}} and spintax stay as written.
categorystring A label you file the template under. Lower case letters, digits, dashes and underscores, at most 40 characters. general when you don’t set one.
is_sharedinteger Always 0 for a template you create.
last_used_attimestamp When a use was last counted. null until the first one.
created_attimestamp When the template was created.
# List templates
Returns your account’s templates, newest first. Not paginated.
Returns
dataobject[] A list of Template objects . An empty list means you have no templates.
Errors
HTTP Code When
500 TEMPLATES_LOAD_FAILEDThe list couldn’t be read. Try again; it doesn’t mean you have no templates.
curl "https://api.tryready.com/templates" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/templates', {
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/templates",
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}')
Returns:
{
"success": true,
"data": [
{
"id": 412,
"user_id": 1042,
"name": "Appointment reminder",
"body": "Hi {{contact.first_name}}, see you tomorrow at 10am. Reply STOP to opt out.",
"category": "reminders",
"is_shared": 0,
"use_count": 7,
"last_used_at": "2026-11-09T15:22:04.000Z",
"created_at": "2026-10-02T13:40:11.000Z"
}
]
}
# Create a template
Saves a new template on your account. Needs send_sms. If you send the same name again within 10 seconds, you get the first template back with deduped: true and no second one is made.
Parameters
namestring Required The template name, up to 100 characters. Leading and trailing spaces are trimmed.
bodystring Required The message text, up to 1,600 characters. Saved as written, with merge fields and spintax kept.
categorystring A label to file it under. Ready lowercases it, turns spaces into dashes, drops other characters and keeps the first 40. Empty or missing becomes general.
Returns
dataobject The new template: id, user_id, name, body, category, is_shared (0) and use_count (0). On a repeat it holds only id, user_id, name and body of the first template.
dedupedboolean true when this was a repeat within 10 seconds. Left out otherwise.
Errors
HTTP Code When
400 invalid_fieldname is missing or isn’t text.
400 none name or body is empty. The answer has an error sentence and no code.
400 TEMPLATE_NOT_TEXTbody isn’t text.
400 TEMPLATE_NAME_TOO_LONGname is over 100 characters.
400 TEMPLATE_BODY_TOO_LONGbody is over 1,600 characters.
400 CONTENT_BLOCKEDThe text breaks the content policy. Not saved. reasons and matched say what was flagged. A blocked save can lower your account’s daily sending limit.
500 TEMPLATE_SAVE_FAILEDThe template couldn’t be saved. Try again.
503 CONTENT_REVIEW_UNAVAILABLEThe content check couldn’t finish. Not saved, and nothing is wrong with the text. Try again shortly.
curl -X POST "https://api.tryready.com/templates" \
-H "X-API-Key: $READY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Appointment reminder",
"body": "Hi {{contact.first_name}}, see you tomorrow at 10am. Reply STOP to opt out.",
"category": "reminders"
}'
const res = await fetch('https://api.tryready.com/templates', {
method: 'POST',
headers: {
'X-API-Key': process.env.READY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Appointment reminder",
"body": "Hi {{contact.first_name}}, see you tomorrow at 10am. Reply STOP to opt out.",
"category": "reminders"
}),
});
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/templates",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
json={
"name": "Appointment reminder",
"body": "Hi {{contact.first_name}}, see you tomorrow at 10am. Reply STOP to opt out.",
"category": "reminders"
},
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": 412,
"user_id": 1042,
"name": "Appointment reminder",
"body": "Hi {{contact.first_name}}, see you tomorrow at 10am. Reply STOP to opt out.",
"category": "reminders",
"is_shared": 0,
"use_count": 0
}
}
# Update a template
Changes a template’s name, text or category. Needs send_sms. Send only the fields you want to change; a missing or empty field keeps its saved value.
Parameters
idinteger, path Required The template id.
namestring The new name, up to 100 characters. Trimmed.
bodystring The new text, up to 1,600 characters. New text goes through the content check; unchanged text does not.
categorystring The new category, cleaned the same way as on create. A value with nothing usable left keeps the old category.
Returns
successboolean true. The template isn’t returned; read it with List templates .
Errors
HTTP Code When
400 invalid_fieldid isn’t a whole number, or a body field is a list or an object.
400 TEMPLATE_STARTER_READONLYThe id is a Ready starter template, not one of yours.
400 TEMPLATE_NOT_TEXTname or body isn’t text.
400 TEMPLATE_NAME_TOO_LONGname is over 100 characters.
400 TEMPLATE_BODY_TOO_LONGbody is over 1,600 characters.
400 CONTENT_BLOCKEDThe new text breaks the content policy. Nothing changed; the old version is still saved.
404 TEMPLATE_NOT_FOUNDNo such template on your account, or it was deleted.
500 TEMPLATE_LOAD_FAILEDThe template couldn’t be read. Nothing changed. Try again.
500 TEMPLATE_SAVE_FAILEDThe change couldn’t be saved. Try again.
503 CONTENT_REVIEW_UNAVAILABLEThe content check couldn’t finish. Nothing changed. Try again shortly.
curl -X PUT "https://api.tryready.com/templates/412" \
-H "X-API-Key: $READY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"body": "Hi {{contact.first_name}}, see you tomorrow at 11am. Reply STOP to opt out."
}'
const res = await fetch('https://api.tryready.com/templates/412', {
method: 'PUT',
headers: {
'X-API-Key': process.env.READY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"body": "Hi {{contact.first_name}}, see you tomorrow at 11am. Reply STOP to opt out."
}),
});
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/templates/412",
headers={"X-API-Key": os.environ["READY_API_KEY"]},
json={
"body": "Hi {{contact.first_name}}, see you tomorrow at 11am. Reply STOP to opt out."
},
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
}
# Delete a template
Deletes one of your templates. Needs send_sms. No body. Deleting it again answers 404 TEMPLATE_NOT_FOUND.
Parameters
idinteger, path Required The template id.
Errors
HTTP Code When
400 invalid_fieldid isn’t a whole number.
400 TEMPLATE_STARTER_READONLYThe id is a Ready starter template, not one of yours.
404 TEMPLATE_NOT_FOUNDNo such template on your account, or it was already deleted.
500 TEMPLATE_LOAD_FAILEDThe template couldn’t be read. Nothing was deleted. Try again.
500 TEMPLATE_DELETE_FAILEDThe template couldn’t be deleted. Try again.
curl -X DELETE "https://api.tryready.com/templates/412" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/templates/412', {
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/templates/412",
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}')
Returns:
{
"success": true
}
# Count a template use
Adds one to the template’s use_count and sets last_used_at to now. Needs send_sms. No body. It doesn’t send a message or fill in merge fields; call it after you send with the template if you want the count to stay accurate. Each call counts one more use.
Parameters
idinteger, path Required The template id.
Errors
HTTP Code When
400 invalid_fieldid isn’t a whole number.
404 TEMPLATE_NOT_FOUNDNo such template on your account, or it was deleted. Nothing was counted.
500 TEMPLATE_LOAD_FAILEDThe template couldn’t be read. Nothing was counted. Try again.
500 none The count couldn’t be saved. The answer has an error sentence and no code.
curl -X POST "https://api.tryready.com/templates/412/use" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/templates/412/use', {
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/templates/412/use",
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}')
Returns:
{
"success": true
}
# List suggested templates
Returns up to five messages you send often that aren’t saved as templates yet. Ready reads the text of your recent campaigns (every A/B version) and drip steps as you wrote them, so merge fields and spintax stay in. Texts that match apart from case and spacing count as one. A text needs at least two uses to show up. The most used come first.
Returns
dataobject[] The suggestions. An empty list means nothing you send often is unsaved.
data[].bodystring The text, as in its most recent use.
data[].times_usedinteger How many campaigns, A/B versions and drip steps used this text.
data[].namestring A suggested name: the first few words, without merge fields or spintax.
data[].merge_fieldsstring[] The merge fields in the text, for example contact.first_name.
data[].has_spintaxboolean Whether the text has spintax.
Errors
HTTP Code When
500 TEMPLATE_SUGGESTIONS_LOAD_FAILEDThe suggestions couldn’t be read. Try again.
curl "https://api.tryready.com/templates/suggested" \
-H "X-API-Key: $READY_API_KEY"
const res = await fetch('https://api.tryready.com/templates/suggested', {
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/templates/suggested",
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}')
Returns:
{
"success": true,
"data": [
{
"body": "Hi {{contact.first_name}}, {our|this} week only: 20% off. Reply STOP to opt out.",
"times_used": 4,
"name": "Hi, week only: 20% off. Reply",
"merge_fields": [
"contact.first_name"
],
"has_spintax": true
}
]
}