The Ingrain API lets your own scripts and tools work with your procedures and runs. It reads procedures and runs, writes procedures, starts runs, and ticks and skips tasks.
The API speaks JSON over HTTPS. The base URL is:
https://ingrain.io/api/v1
Send the token with each request:
Authorization: Bearer <token>
Accept: application/json
A token can do what you can do, in every team you belong to. Keep it secret.
A token lasts one year. You can revoke it on the same page at any time.
curl https://ingrain.io/api/v1/teams \
-H "Authorization: Bearer $INGRAIN_TOKEN" \
-H "Accept: application/json"
{
"data": [
{ "id": 12, "name": "Kitchen", "role": "admin", "personal": false }
]
}
member, manager or admin.| Method | Path | What it does |
|---|---|---|
GET |
/teams |
Lists your teams. |
GET |
/procedures |
Lists procedures. |
GET |
/procedures/{id} |
Gets one procedure and its steps. |
POST |
/procedures |
Makes a procedure. |
PATCH |
/procedures/{id} |
Changes a procedure. |
DELETE |
/procedures/{id} |
Deletes a procedure. |
GET |
/runs |
Lists runs. |
GET |
/runs/{id} |
Gets one run and its tasks. |
POST |
/procedures/{id}/runs |
Starts a run. |
PATCH |
/runs/{id}/tasks |
Ticks or skips tasks in a run. |
Each endpoint covers every team you belong to. A row in a team you do not belong to answers 404.
GET /teams
Gives your teams, sorted by name, with your role in each. personal is true for your own personal team.
GET /procedures
| Parameter | Type | Required | Meaning |
|---|---|---|---|
team_id |
integer | no | Only procedures in this team. |
per_page |
integer | no | Rows per page, 1 to 100. The default is 50. |
GET /procedures/{id}
{
"data": {
"id": 31,
"name": "Closing the kitchen",
"description": "Do this at the end of each shift.",
"team": "Kitchen",
"team_id": 12,
"lead": "Kari Nordmann",
"lead_id": 7,
"tasks": [
{ "id": 401, "type": "Heading", "content": "Floor", "assignee": null, "sort_order": 0 },
{ "id": 402, "type": "Check", "content": "Wash the floor", "assignee": null, "sort_order": 1 }
]
}
}
POST /procedures
You must be an admin in the team.
| Field | Type | Required | Meaning |
|---|---|---|---|
team_id |
integer | yes | The team that gets the procedure. |
name |
string | yes | Up to 200 characters. |
description |
string | no | Up to 2000 characters. |
lead_id |
integer | no | The user who leads the procedure. |
steps |
array of strings | no | The steps, in order. |
A step that starts with # becomes a heading. Each other step becomes a task to tick.
curl https://ingrain.io/api/v1/procedures \
-H "Authorization: Bearer $INGRAIN_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"team_id": 12,
"name": "Closing the kitchen",
"steps": ["# Floor", "Wash the floor", "Empty the bins"]
}'
The answer is 201 with the new procedure, in the same shape as Get a procedure.
PATCH /procedures/{id}
You must be an admin in the team, or the person who made the procedure.
Send only the fields to change: name, description, lead_id or steps. The fields work as in Make a procedure.
Sending steps replaces the whole list of steps. The new steps get new IDs.
DELETE /procedures/{id}
You must be an admin in the team, or the person who made the procedure.
This deletes the procedure with its steps, its schedules and its runs. The answer is 204 with no body.
GET /runs
Gives runs, newest first.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
procedure_id |
integer | no | Only runs of this procedure. |
status |
string | no | active or done. |
per_page |
integer | no | Rows per page, 1 to 100. The default is 50. |
GET /runs/{id}
{
"data": {
"id": 880,
"name": null,
"description": null,
"procedure": "Closing the kitchen",
"procedure_id": 31,
"team": "Kitchen",
"status": "active",
"assignee": "Kari Nordmann",
"due_at": "2026-10-20T22:00:00+00:00",
"completed_at": null,
"completion": 50,
"tasks": [
{
"id": 9001,
"type": "Check",
"content": "Wash the floor",
"status": "done",
"skip_reason": null,
"progress": 0,
"assignee": null,
"completed_by": "Kari Nordmann",
"completed_at": "2026-10-20T21:14:03+00:00",
"sort_order": 1
}
]
}
}
completion is the share of tasks that are done or skipped, from 0 to 100.
A task status is open, done or skipped. A task type is Check, Progress, Heading or Markdown.
Times are in ISO 8601 with the time zone.
POST /procedures/{id}/runs
You must be a manager or an admin in the team, or the lead of the procedure.
| Field | Type | Required | Meaning |
|---|---|---|---|
name |
string | no | Up to 200 characters. Without a name, the run has no name of its own. |
description |
string | no | Up to 2000 characters. |
due_at_date |
string | no | The due date, as YYYY-MM-DD. |
due_at_time |
string | no | The due time, as HH:MM. |
The due date and time are in your own time zone, as set in your Ingrain profile.
Ingrain tells the team about the new run. The answer is 201 with the new run, in the same shape as Get a run.
PATCH /runs/{id}/tasks
Sets tasks in one run to open, done or skipped.
{
"tasks": [
{ "id": 9001, "status": "done" },
{ "id": 9002, "status": "skipped", "reason": "The bins were already empty." },
{ "id": 9003, "status": "open" }
]
}
| Field | Type | Required | Meaning |
|---|---|---|---|
tasks[].id |
integer | yes | The task ID, from Get a run. |
tasks[].status |
string | yes | open, done or skipped. |
tasks[].reason |
string | for a skip | Why the task is skipped. Up to 1000 characters. |
Only tasks of type Check and Progress can change.
A skip tells the team, as in the app. To change the reason, skip the task again.
A done task cannot be skipped. Set it to open first.
To undo a skip, set the task to open or done. The old skip stays on record.
You may change a task when you are in the team of the run.
Ingrain checks every task before it changes any. If one task is wrong, the request changes nothing and answers 422.
The answer is the run, in the same shape as Get a run.
A list comes in pages. The answer puts the rows under data, and the page facts under links and meta:
{
"data": [ ],
"links": { "first": "…", "last": "…", "prev": null, "next": "…" },
"meta": { "current_page": 1, "last_page": 3, "per_page": 50, "total": 120 }
}
To get the next page, follow links.next. It is null on the last page.
Each error answers in JSON.
| Status | Meaning |
|---|---|
200 |
Done. |
201 |
Made. |
204 |
Deleted. |
401 |
The token is missing, expired or revoked. |
403 |
Your role does not allow this. |
404 |
No such row in your teams. |
422 |
The input is wrong. See errors. |
429 |
Too many requests. Wait and try again. |
A 422 answer names each wrong field:
{
"message": "The name field is required.",
"errors": {
"name": ["The name field is required."]
}
}
You can send 60 requests a minute. All your tokens share this limit.
When you go over the limit, the answer is 429. The Retry-After header gives the seconds to wait.