Ingrain API

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

Get a token

  1. Log in to Ingrain.
  2. Open API tokens from the menu under your name.
  3. Give the token a name and click Make token.
  4. Copy the token. It shows only once.

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.

Your first request

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 }
  ]
}

Words

  • A team is a group of people who share procedures.
  • A procedure is a named list of steps.
  • A run is one pass through the steps of a procedure. It has a due date, and each step becomes a task.
  • A role is what you may do in a team: member, manager or admin.

Endpoints

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.

List teams

GET /teams

Gives your teams, sorted by name, with your role in each. personal is true for your own personal team.

List procedures

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 a procedure

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 }
    ]
  }
}

Make a procedure

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.

Change 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 a procedure

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.

List runs

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 a run

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.

Start a run

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.

Update tasks

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.

Pages

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.

Errors

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."]
  }
}

Limits

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.