Quick start
Create a key on the API keys page (Integrations → API keys) and pick, per resource, whether the key gets no access, read-only or read and write. The key is shown once, at creation; its permissions can be changed later without reissuing it. Then check it works:
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://household.email/api/me"
Which answers with the household the key belongs to:
{
"household": { "id": "0f2c…", "name": "The Cipollas" },
"user": { "id": "8ab1…", "email": "you@example.com", "initials": "MC" },
"api_key": { "name": "Home Assistant", "permissions": ["calendar:read", "todos"] }
}
Base URL
https://household.email/api
Conventions
- Request and response bodies are JSON. Send
Content-Type: application/jsonon writes. - Write bodies are wrapped in the resource name:
{"todo": {…}},{"event": {…}},{"shopping_list": {…}},{"item": {…}}. - IDs are UUIDs. Timestamps are ISO 8601; dates are
YYYY-MM-DD. - Updates accept
PATCHorPUTand take only the fields you are changing. - Deletes answer
204 No Contentwith an empty body. - A key belongs to a household, not to a person. Records created through the API are attributed to the household's representative adult.
Authentication
Every request carries the key in the Authorization
header. Both forms are accepted:
Authorization: Bearer YOUR_API_KEY
Authorization: YOUR_API_KEY
Keys are stored hashed, so a lost key cannot be recovered — delete it and make another. A key can be given an expiry when it is created; an expired key is rejected the same way an unknown one is. Keep keys out of version control.
Issuing a key from an app: the magic-link flow
An app that signs a household member in — rather than being configured with a key by hand — can mint its own key in two steps. This is the flow the native iOS and Android shells use. Neither step needs an existing key.
POST /api/auth/magic_link
Email a sign-in link to a household member.
Requires None
Body
| Name | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | The member's email address. Matched case-insensitively. |
env_label |
string | No | Label shown in the email, so a test build's link is distinguishable from production's. |
curl -X POST \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com"}' \
"https://household.email/api/auth/magic_link"
{ "sent": true }
The answer is always {"sent": true}, even for an
address that has no account. That is deliberate: the endpoint is unauthenticated, and it
must not tell a stranger who has an account here.
POST /api/auth/exchange
Trade the token from the emailed link for a new API key.
Requires None
Body
| Name | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | The token from the magic link. Single use, and valid for 30 minutes. |
device_label |
string | No | Names the key, as "Mobile — <label>". Defaults to "unnamed device". |
curl -X POST \
-H "Content-Type: application/json" \
-d '{"token":"THE_TOKEN","device_label":"iPhone"}' \
"https://household.email/api/auth/exchange"
{
"api_key": "3f9c…",
"household": { "id": "0f2c…", "name": "The Cipollas" },
"user": { "id": "8ab1…", "email": "you@example.com", "initials": "MC" }
}
The key comes back once and is not retrievable afterwards. It is granted read and write
on every resource, so treat it like the sign-in it stands in for. Replaying a token,
using one older than 30 minutes, or sending a token that was never issued all answer
401 with
{"error": "Invalid or expired token"}. A member
with no household gets 422.
GET /api/me
The household, the person the key belongs to, and the key's permissions.
Requires Any active key
The one endpoint that ignores permissions: any active key on a Pro household can call it. Use it to check a key is live and to find out what it may do before trying it.
Permissions
A key carries a comma-separated list of permissions. The key form offers, per resource, no access, read-only, or read and write:
| Resource | Read-only | Read & write |
|---|---|---|
calendar |
calendar:read |
calendar
|
todos |
todos:read |
todos
|
chores |
chores:read |
chores
|
shopping_lists |
shopping_lists:read |
shopping_lists
|
recipes |
recipes:read |
Read-only resource |
meal_plans |
meal_plans:read |
Read-only resource |
emails |
emails |
Read-only resource |
A coarse permission implies every fine-grained one below it: a key holding
calendar satisfies
calendar:read,
calendar:create,
calendar:update and
calendar:delete. Calendar and todos can also be
granted one verb at a time by writing the fine-grained permission on its own.
Two exceptions are worth knowing before you build against them. The REST endpoints for
chores and shopping lists check the coarse
chores and
shopping_lists permissions for reads as well as
writes, so a read-only key on those resources cannot call them over HTTP — it can still
read them through the MCP tools, which check
chores:read and
shopping_lists:read. And
emails grants no REST endpoints at all; it exists
so an MCP client can search your inbox.
A key never reaches beyond the household it was created in. Asking for another
household's record answers 404, not
403.
Endpoint index
Every endpoint the API serves, and the permission each one checks.
| Method | Path | Permission | What it does |
|---|---|---|---|
| Authentication | |||
| POST | /api/auth/magic_link | None |
Email a sign-in link to a household member. |
| POST | /api/auth/exchange | None |
Trade a magic-link token for a new API key. |
| Identity | |||
| GET | /api/me | Any active key |
The household, the person the key belongs to, and the key's permissions. |
| Glance | |||
| GET | /api/glance | Any active key |
Today at a glance, for home-screen widgets: one payload carrying the sections the key may read, each with the summary line the app's own tiles show. Answers a repeat request with a 304. |
| Calendar | |||
| GET | /api/calendar | calendar:read |
Events starting in a date range. |
| GET | /api/calendar/:id | calendar:read |
One event. |
| POST | /api/calendar | calendar:create |
Create an event. |
| PATCH | /api/calendar/:id | calendar:update |
Update an event. |
| DELETE | /api/calendar/:id | calendar:delete |
Delete an event. |
| Todos | |||
| GET | /api/todos | todos:read |
The household's todos, optionally filtered. |
| GET | /api/todos/:id | todos:read |
One todo. |
| POST | /api/todos | todos:create |
Create a todo. |
| PATCH | /api/todos/:id | todos:update |
Update a todo. |
| DELETE | /api/todos/:id | todos:delete |
Delete a todo. |
| Chores | |||
| GET | /api/chores | chores |
Chores with whose turn it is, plus the last week of completions. |
| POST | /api/chores/:id/complete | chores |
Record a completion and roll the chore forward. |
| Shopping lists | |||
| GET | /api/shopping_lists | shopping_lists |
Every list with its items. |
| GET | /api/shopping_lists/:id | shopping_lists |
One list with its items. |
| POST | /api/shopping_lists | shopping_lists |
Create a list. |
| PATCH | /api/shopping_lists/:id | shopping_lists |
Rename a list. |
| DELETE | /api/shopping_lists/:id | shopping_lists |
Delete a list and its items. |
| POST | /api/shopping_lists/:shopping_list_id/items | shopping_lists |
Add an item to a list. |
| PATCH | /api/shopping_lists/:shopping_list_id/items/:id | shopping_lists |
Update an item. |
| PATCH | /api/shopping_lists/:shopping_list_id/items/:id/toggle | shopping_lists |
Tick an item off, or put it back on. |
| DELETE | /api/shopping_lists/:shopping_list_id/items/:id | shopping_lists |
Remove an item. |
| Recipes | |||
| GET | /api/recipes | recipes:read |
The recipe book in title order, without the lines or the method. |
| GET | /api/recipes/:id | recipes:read |
One recipe in full, with every ingredient line and the method. |
| Meal plan | |||
| GET | /api/meal_plan_entries | meal_plans:read |
The meals planned between two dates, in date and meal order. |
| MCP | |||
| GET, POST, DELETE | /api/mcp | Per tool |
Model Context Protocol endpoint, Streamable HTTP transport. |
Calendar
GET /api/calendar
Events starting between two dates, inclusive of both days.
Requires calendar:read
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
start |
date | Yes | First day of the window, YYYY-MM-DD. |
end |
date | Yes | Last day of the window, YYYY-MM-DD. Must not be before start. |
Both are required: a missing, unparseable or reversed range answers
400. Events are matched on their own
start_at; a recurring series is returned once,
as the series, rather than expanded into occurrences.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://household.email/api/calendar?start=2026-09-01&end=2026-09-30"
{
"events": [ { … } ],
"meta": { "start": "2026-09-01", "end": "2026-09-30", "count": 12 }
}
GET /api/calendar/:id
One event, wrapped as { "event": { … } }.
Requires calendar:read
POST /api/calendar
Create an event. Answers 201 with the created event.
Requires calendar:create
Body, under event
| Name | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Event title. |
start_at |
datetime | Yes | When it starts, ISO 8601. |
end_at |
datetime | No | When it ends, ISO 8601. |
description |
string | No | Free text. |
all_day |
boolean | No | Whether it takes the whole day. |
location |
string | No | Where it is. |
event_type |
string | No | One of event, birthday, anniversary. Defaults to event. |
rrule |
string | No | iCalendar recurrence rule, e.g. FREQ=WEEKLY;BYDAY=TU. |
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event":{"title":"Swimming","start_at":"2026-09-15T16:00:00Z","end_at":"2026-09-15T17:00:00Z","location":"Aquatic centre"}}' \
"https://household.email/api/calendar"
PATCH /api/calendar/:id
Update an event. Send only the fields you are changing.
Requires calendar:update
DELETE /api/calendar/:id
Delete an event. Answers 204.
Requires calendar:delete
Event shape
{
"event": {
"id": "9a1e…",
"title": "Swimming",
"description": null,
"start_at": "2026-09-15T16:00:00Z",
"end_at": "2026-09-15T17:00:00Z",
"all_day": false,
"event_type": "event",
"location": "Aquatic centre",
"rrule": null,
"from_todo": false,
"created_at": "2026-09-01T09:12:41Z",
"updated_at": "2026-09-01T09:12:41Z"
}
}
from_todo marks an event the app created from a
todo's due date, so a client can tell it apart from one somebody entered.
Todos
GET /api/todos
The household's todos in display order.
Requires todos:read
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status |
string | No | active, completed or cancelled. |
priority |
string | No | low, medium, high or urgent. |
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://household.email/api/todos?status=active&priority=urgent"
{ "todos": [ { … } ], "meta": { "count": 3 } }
GET /api/todos/:id
One todo, wrapped as { "todo": { … } }.
Requires todos:read
POST /api/todos
Create a todo. Answers 201 with the created todo.
Requires todos:create
Body, under todo
| Name | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | What needs doing. |
description |
string | No | Free text. |
due_date |
datetime | No | When it is due, ISO 8601. |
all_day |
boolean | No | Whether the due date is a whole day rather than a time. |
priority |
string | No | low, medium, high or urgent. Defaults to medium. |
status |
string | No | active, completed or cancelled. Defaults to active. |
recurrence |
string | No | daily, weekly, fortnightly, monthly or yearly. Omit for a one-off; completing a repeating todo creates the next occurrence with the same assignees. |
auto_create_calendar_event |
boolean | No | Also put the due date on the household calendar. |
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"todo":{"title":"Pay the water bill","priority":"high","due_date":"2026-09-30T00:00:00Z"}}' \
"https://household.email/api/todos"
PATCH /api/todos/:id
Update a todo. Send only the fields you are changing.
Requires todos:update
DELETE /api/todos/:id
Delete a todo. Answers 204.
Requires todos:delete
Todo shape
{
"todo": {
"id": "4c77…",
"title": "Pay the water bill",
"description": null,
"status": "active",
"priority": "high",
"assignment_type": "unassigned",
"recurrence": null,
"due_date": "2026-09-30T00:00:00Z",
"position": 1,
"assigned_adult_ids": [],
"overdue": false,
"from_email": false,
"created_at": "2026-09-01T09:12:41Z",
"updated_at": "2026-09-01T09:12:41Z"
}
}
assignment_type is
unassigned,
single or
multiple (two or more assignees — todos do not
rotate), and
from_email marks a todo the app pulled out of a
message. Assignments are read-only here: the API reports
assigned_adult_ids but does not set them.
Chores
Chores are recurring, and can rotate between household members. Both endpoints need the
coarse chores permission.
GET /api/chores
Chores by next due date, with whose turn it is worked out for you.
Requires chores
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
overdue |
boolean | No | Pass true for only the chores past their due date. |
due_today |
boolean | No | Pass true for only the chores due today. Ignored if overdue=true. |
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://household.email/api/chores?due_today=true"
{
"chores": [
{
"id": "b410…",
"title": "Take the bins out",
"description": null,
"frequency": "weekly",
"frequency_label": "Weekly",
"next_due_date": "2026-09-15",
"formatted_next_due_date": "Tue 15 Sep",
"has_rotation": true,
"current_assignee_id": "8ab1…",
"current_assignee_name": "Mark",
"rotation_assignees": [ { "id": "8ab1…", "name": "Mark", "type": "Adult" } ],
"overdue": false,
"position": 0,
"created_at": "2026-08-01T09:12:41Z",
"updated_at": "2026-09-08T20:03:02Z"
}
],
"recent_completions": [
{
"id": "d902…",
"chore_id": "b410…",
"chore_title": "Take the bins out",
"completer_id": "8ab1…",
"completer_name": "Mark",
"completer_type": "Adult",
"completed_at": "2026-09-08T20:03:02Z"
}
],
"meta": { "count": 1 }
}
recent_completions covers the last seven days,
newest first, across every chore in the household — not just the ones in
chores.
frequency is one of
daily,
weekly,
fortnightly or
monthly.
POST /api/chores/:id/complete
Record a completion and move the chore to its next due date.
Requires chores
Body
| Name | Type | Required | Description |
|---|---|---|---|
completer_id |
uuid | No | Who did it. Defaults to the household's representative adult. |
completer_type |
string | No | Adult or Child. Required alongside completer_id. |
create_todo |
boolean | No | Whether to create a todo for whoever's turn is next. Defaults to true; pass "false" to skip it. |
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"completer_id":"8ab1…","completer_type":"Adult","create_todo":"false"}' \
"https://household.email/api/chores/b410…/complete"
{ "chore": { … }, "completion": { … } }
Chores are read and completed through the API but not created, changed or deleted by it;
use the app for that. A completer that does not belong to the household answers
422 with
{"error": "No completer available"}.
Shopping lists
Every endpoint here checks the coarse
shopping_lists permission, reads included.
GET /api/shopping_lists
Every list, in display order, each with its items inline.
Requires shopping_lists
GET /api/shopping_lists/:id
One list with its items.
Requires shopping_lists
POST /api/shopping_lists
Create a list. Answers 201.
Requires shopping_lists
Body, under shopping_list
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | What the list is called. |
PATCH /api/shopping_lists/:id
Rename a list.
Requires shopping_lists
DELETE /api/shopping_lists/:id
Delete a list and everything on it. Answers 204.
Requires shopping_lists
POST /api/shopping_lists/:shopping_list_id/items
Add an item to a list. Answers 201.
Requires shopping_lists
Body, under item
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | What to buy. |
quantity |
string | No | Free text, e.g. "2 kg" or "12". |
checked |
boolean | No | Whether it is already in the trolley. |
label_names |
array of strings | No | Shop or aisle labels. Labels that do not exist yet are created. |
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"item":{"name":"Milk","quantity":"2 L","label_names":["Woolworths"]}}' \
"https://household.email/api/shopping_lists/7d31…/items"
PATCH /api/shopping_lists/:shopping_list_id/items/:id
Update an item.
Requires shopping_lists
PATCH /api/shopping_lists/:shopping_list_id/items/:id/toggle
Flip an item between bought and not bought. Takes no body.
Requires shopping_lists
DELETE /api/shopping_lists/:shopping_list_id/items/:id
Remove an item. Answers 204.
Requires shopping_lists
List shape
{
"shopping_list": {
"id": "7d31…",
"name": "Groceries",
"primary": true,
"position": 0,
"items": [
{
"id": "c55a…",
"shopping_list_id": "7d31…",
"name": "Milk",
"quantity": "2 L",
"checked": false,
"labels": ["Woolworths"],
"position": 0,
"created_at": "2026-09-10T07:41:00Z",
"updated_at": "2026-09-10T07:41:00Z"
}
],
"items_count": 1,
"checked_count": 0,
"created_at": "2026-08-02T11:00:00Z",
"updated_at": "2026-09-10T07:41:00Z"
}
}
The collection endpoint wraps the same objects as
{"shopping_lists": [ … ], "meta": {"count": n}},
and the item endpoints answer {"item": { … }}.
Recipes
Read-only. Recipes are written, imported and structured in the app — that is a workflow
rather than a single request — so the API reads the book without writing to it. Only
recipes:read exists as a permission; there is no
coarse recipes, deliberately, so a key granted
today cannot silently widen into write access the day write endpoints land.
These endpoints check two plan features, not one: API access, and meal
planning. A Pro household has both; a household with API access but not meal planning
gets 403 with
{"error": "Plan upgrade required"}.
GET /api/recipes
The recipe book in title order, without the ingredient lines or the method.
Requires recipes:read
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
q |
string | No | Narrow by keyword. Matches titles, descriptions and ingredients. |
category |
string | No | Narrow to one kind of dish. One of breakfast, lunch, dinner, side, snack, dessert, cake, drink, sauce. An unrecognised value returns no recipes rather than all of them. |
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://household.email/api/recipes?q=aubergine"
{ "recipes": [ { … } ], "meta": { "count": 2 } }
GET /api/recipes/:id
One recipe in full: the summary, the method, and every ingredient line.
Requires recipes:read
{
"recipe": {
"id": "e18f…",
"title": "Bolognese",
"description": "The Sunday one.",
"servings": 4,
"prep_minutes": 15,
"cook_minutes": 120,
"total_minutes": 135,
"source_url": "https://example.com/bolognese",
"source_name": "example.com",
"source_label": "example.com",
"ingredients_count": 12,
"categories": ["dinner"],
"category_labels": ["Dinner"],
"allergens": [
{ "allergen": "milk", "label": "Milk", "ingredients": ["100 ml whole milk"] }
],
"instructions": "Soffritto first…",
"ingredients": [
{
"id": "aa20…",
"text": "2 cups finely chopped onion, divided",
"quantity": "2",
"unit": "cups",
"food_name": "onion",
"note": "finely chopped, divided",
"position": 0,
"allergens": []
}
],
"created_at": "2026-09-02T18:20:00Z",
"updated_at": "2026-09-02T18:20:00Z"
}
}
Each ingredient keeps both forms: text is the line
as a person reads it and is always present, while
quantity,
unit and
food_name are filled in only where they could be
parsed out of it. A line nothing could be parsed from still arrives, unstructured — so
scale arithmetic has to fall back to text rather
than assume a quantity is there.
Categories
A recipe carries several categories, not one — a soup is lunch and dinner, a
lemon drizzle is a cake and a dessert — so
categories is an array and is frequently empty:
nothing obliges a household to file its recipes. Treat an empty array as "not filed",
never as a kind of its own, and do not expect
?category= to return it.
The vocabulary is closed, so a filter can be relied on to mean the same thing in every
household: breakfast, lunch, dinner, side, snack, dessert, cake, drink, and sauce.
category_labels is the same list as it reads in the
interface, sent alongside so a client displaying them does not have to keep its own copy
of the vocabulary in step with ours.
Allergens
Both the summary and the full recipe carry an
allergens array: the allergens an ingredient's
name matched, each with the lines responsible, so a reader can check the match
instead of taking it on trust. The asymmetry matters and the API will not paper over it —
a match is a suspicion worth raising; an empty array is not an assurance that a
recipe is free from anything. Matching names cannot establish that. Do not
present an empty array as "free from".
Meal plan
Read-only, and gated on meal planning as well as API access, exactly as recipes are. A week is not a record — every entry carries its own date — so there is no plan resource to fetch, only the entries between two days.
GET /api/meal_plan_entries
The meals planned between two dates, in date and meal order.
Requires meal_plans:read
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
start_date |
date | No | First day to include, YYYY-MM-DD. Give both dates or neither. |
end_date |
date | No | Last day to include, YYYY-MM-DD. Omit both for this week, Monday to Sunday. |
One date without the other is an error rather than a half-open range, and a date that
will not parse is rejected rather than quietly falling back to this week — a client that
sent one asked for a range it is not getting. Both answer
400: "start_date and end_date must be dates formatted YYYY-MM-DD",
or "start_date must be on or before end_date" for a
backwards range.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://household.email/api/meal_plan_entries?start_date=2026-09-14&end_date=2026-09-20"
{
"meal_plan_entries": [
{
"id": "5b7c…",
"date": "2026-09-15",
"meal_type": "dinner",
"title": "Bolognese",
"servings": 6,
"spare_servings": 4,
"spare_remaining": 2,
"scale_factor": 2.5,
"leftover": false,
"leftover_of_id": null,
"cooked_on": null,
"use_by_date": null,
"recipe": { "id": "e18f…", "title": "Bolognese", "allergens": [] },
"created_at": "2026-09-10T08:00:00Z",
"updated_at": "2026-09-10T08:00:00Z"
}
],
"meta": { "start_date": "2026-09-14", "end_date": "2026-09-20", "count": 1 }
}
meal_type is
breakfast,
lunch,
snack,
dinner or
dessert. An entry can stand on its own without a
recipe — "Takeaway", "Leftovers" and "Eating at Nan's" are real parts of a week — so
recipe may be
null, and
scale_factor is
null with it: there is nothing to scale.
title is always populated — the entry's own name
where it has one, otherwise the recipe's.
servings is what that sitting makes, which
can differ from the recipe's own count. The
nested recipe is the same summary shape
GET /api/recipes returns, allergens included, so a
whole week can be checked against a household's restrictions in one request.
spare_servings is what the meal makes on top
of that sitting, for the fridge, and
spare_remaining is how much of it nothing later
has claimed yet — "how many serves of the bolognese are left" in one field.
scale_factor is the two counts together over the
recipe's own, because what is cooked spare is still cooked: the example above is six for
the table and four for the fridge, from a recipe written for four.
An entry with leftover_of_id set is eating out of
that meal rather than being cooked again, and takes its
cooked_on from it.
use_by_date on a leftovers entry is four days from
cooked_on; it is
informational, and nothing validates, hides or expires against it.
MCP (AI assistants)
The same data is served over the Model Context Protocol, so an assistant that speaks MCP can work with your household directly. It authenticates with the same API keys, over the Streamable HTTP transport:
https://household.email/api/mcp
The key's permissions decide which tools the assistant is even offered — a read-only key gets the list and search tools and nothing else, so an assistant cannot change what it was never given access to.
| Tool | Required permission |
|---|---|
search_household |
Any read permission. It searches only the sources the key can read — emails need emails. |
list_calendar_events | calendar:read |
list_todos | todos:read |
list_chores | chores:read |
list_shopping_lists | shopping_lists:read |
list_recipes, get_recipe | recipes:read, plus a plan including meal planning |
list_meal_plan_entries | meal_plans:read, plus a plan including meal planning |
create_calendar_event, update_calendar_event, delete_calendar_event | calendar:create, calendar:update, calendar:delete |
create_todo, update_todo, delete_todo | todos:create, todos:update, todos:delete |
add_shopping_item, check_shopping_item, remove_shopping_item | shopping_lists:create, shopping_lists:update, shopping_lists:delete |
complete_chore | chores |
Email content is written by people outside your household, so the server tells assistants
to treat it as information and never as instructions. Grant
emails only to assistants you would trust with
your inbox.
A household on the free plan gets a JSON-RPC error with code
-32003 and HTTP
403, rather than the REST API's bare error object,
so an MCP client can read it.
Connect your assistant
Run in a terminal:
claude mcp add --transport http household-email https://household.email/api/mcp \
--header "Authorization: Bearer YOUR_API_KEY"
Errors
Failures answer with a JSON object. Everything except a validation failure uses a single
error string; validation failures use
errors, an array of sentences fit to show a person.
| Status | When | Body |
|---|---|---|
| 400 | A required parameter is missing or cannot be parsed. | {"error": "Both 'start' and 'end' parameters are required (format: YYYY-MM-DD)"} |
| 401 | No Authorization header, or a key that is unknown, deleted or expired. |
{"error": "API key is missing"} or {"error": "Invalid or expired API key"} |
| 403 | The household is not on Pro. | {"error": "Plan upgrade required"} |
| 403 | The plan is fine, but the key lacks the permission. | {"error": "API key does not have permission to access calendar:create"} |
| 404 | No such record in this household — including records that exist in someone else's. | {"error": "Resource not found"} |
| 422 | A create or update failed validation. | {"errors": ["Title can't be blank"]} |
The plan check runs before the permission check, so a free household is told about its
plan rather than about the key's scopes. Both are
403; the message is what tells them apart.
Rate limits
There are none today. No request budget is enforced and no rate-limit headers are returned, so please be reasonable: poll on a sensible interval and back off on errors. If we add limits we will publish them here before they take effect.
Questions
Email hello@household.email. We're small, so that address reaches a person who can answer.