API reference

Authenticate with a personal access token and read, create and update cards from your own code.

Track40 has an HTTP API. It is the same API the app itself runs on, and the endpoints on this page are the supported integration surface, stable paths you can build against. Everything is JSON over HTTPS.

Base URL:

https://app.track40.com/api

Authentication

Create a personal access token in the app under Settings → Tokens. The token starts with t40_ and is shown once at creation; store it like a password. Send it as a bearer token on every request:

curl https://app.track40.com/api/me \
  -H "Authorization: Bearer t40_your_token_here"

A token acts as you. It sees the teams, pipes and cards you can see, and its writes are attributed to you.

Scopes

When you mint a token you can restrict what it may do. A restricted token can only call the endpoints on this page, each gated by the matching scope; everything else is refused.

Scope Allows
cards:read Read pipes, cards, comments, members, attachments
cards:write Create, update, delete and restore cards
comments:write Post, edit and delete comments
automations:write Manage a pipe’s webhooks

A token minted with no scope restriction has full access. The Pipefy migration tools use their own scopes (imports:write, attachments:write, sync:write); you will see them in the token list but should not need them for your own integrations.

Conventions

  • Ids. Teams use a 5-character code and pipes an 8-character code. Both are opaque, immutable and URL-safe (they appear in app URLs as /t/<team>/pipes/<pipe>). Phases, fields, labels and select options use the same 8-character style inside the pipe document. Cards and comments use UUIDs.
  • Errors. Failures return { "error": "..." } with a conventional status code; validation failures add an issues array. Resources you cannot access return 404, not 403, so ids do not leak existence.
  • Timestamps are ISO 8601 in UTC.

Usage limits

Every request made with a token draws one API call from your team’s monthly allowance. On the Standard plan that is 5,000 calls per editor per month, pooled across the team; past the allowance, calls draw the team wallet at $1 per 1,000. When both are exhausted, write requests return 402 { "code": "plan_limit" } or 402 { "code": "usage_exhausted" }. Reads keep working, so an out-of-allowance team can always get its data out.

Identity

GET /me returns the authenticated user and their teams. It is a sensible first call, because everything else needs a team code to go any further.

{
  "user": { "id": "…", "email": "kim@example.com", "name": "Kim" },
  "teams": [{ "shortId": "ac3me", "name": "Acme", "role": "owner" }]
}

Responses also carry app-internal keys; treat anything not shown here as subject to change.

Pipes

GET /teams/:team/pipes
GET /teams/:team/pipes/:pipe

The list returns each pipe’s code, name, icon, color and active-card count. The detail returns the full pipe document. It carries pipe.phases (each { id, name, kind, color }, where kind is normal or done), pipe.labels, and fields.fields (each { id, label, type, phase, required } plus config.options for selects). Two structural rules worth knowing:

  • A card is always in exactly one phase; phases with kind: "done" are the terminal lanes.
  • The intake form is not a phase: form fields have phase: "$form", and every other field belongs to the phase whose id it names.

The detail response also includes a first page of cards for the app’s board view. For integration reads, use the cards endpoints below instead; they return a friendlier shape.

Cards

List and find

GET /teams/:team/pipes/:pipe/cards

Returns hydrated cards (ids resolved to names; the shape is below), newest first, with cursor pagination.

Query parameter Meaning
sort updated (default) or created: which timestamp orders the list
phaseId Only cards currently in this phase
q Free-text search over title and field values
updatedSince ISO datetime; only cards changed since then
limit Page size, 1–100 (default 50)
cursor Opaque cursor from the previous page
curl "https://app.track40.com/api/teams/ac3me/pipes/sq4mx2ph/cards?q=acme&limit=10" \
  -H "Authorization: Bearer $TOKEN"

The response is { "cards": [...], "nextCursor": "..." }; nextCursor is null on the last page. updatedSince plus sort=updated gives you a cheap polling loop.

Search a whole pipe

GET /teams/:team/pipes/:pipe/search?q=<text>

Returns every match in the pipe as bare ids, ranked by relevance and then by how recently the card changed:

[{ "id": "3f1c9b2e-5a70-4d2b-9e61-8c4f2a7d5b90" }, { "id": "b7a2d40f-…" }]

This one is uncapped and spans the whole pipe, including cards sitting in done phases, so reach for it when you need the complete match set and not just one page. Hydrate the ids you care about with the single-card endpoint below. An empty q returns []. For most integrations the q parameter on the list endpoint above is the better fit, because it returns whole cards in one call.

Read one card

GET /teams/:team/cards/:id/hydrated

Returns one card with everything resolved:

{
  "id": "3f1c9b2e-5a70-4d2b-9e61-8c4f2a7d5b90",
  "title": "Renew the Acme contract",
  "url": "https://app.track40.com/t/ac3me/pipes/sq4mx2ph/cards/3f1c9b2e-…",
  "pipe": { "shortId": "sq4mx2ph", "name": "Sales pipeline" },
  "phase": { "id": "ph2qzk4n", "name": "Negotiation" },
  "labels": [{ "id": "hb7t2mkc", "name": "Priority", "color": "#ef4444" }],
  "createdAt": "2026-08-01T09:12:00.000Z",
  "updatedAt": "2026-08-27T04:30:11.000Z",
  "phaseEnteredAt": "2026-08-20T22:04:09.000Z",
  "dueDate": null,
  "finishedAt": null,
  "createdBy": { "id": "…", "name": "Kim", "email": "kim@example.com" },
  "commentsCount": 3,
  "fields": {
    "cmp4n8ax": {
      "id": "cmp4n8ax",
      "label": "Company",
      "type": "text",
      "value": "Acme Ltd",
      "display": "Acme Ltd"
    }
  },
  "values": { "cmp4n8ax": "Acme Ltd" }
}

fields carries each visible field with its raw value and a human-readable display (option ids become their labels, user ids become names); values is the same data keyed by field id alone.

Create a card

POST /teams/:team/cards
curl -X POST https://app.track40.com/api/teams/ac3me/cards \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "pipeShortId": "sq4mx2ph",
    "values": {
      "nm3e8qtd": "Renew the Acme contract",
      "cmp4n8ax": "Acme Ltd",
      "amt7q2vd": 12000
    }
  }'

values is keyed by field id (read the ids from the pipe detail). Required form fields must be present. The card lands in the pipe’s first phase, and the 201 response returns it under card alongside the current pipe document.

Update a card

PATCH /teams/:team/cards/:id

The body is any combination of:

  • values: field values to set, keyed by field id
  • phaseId: move the card to another phase
  • labels: the card’s label ids (full replacement)
  • dueDate: ISO datetime, or null to clear
curl -X PATCH https://app.track40.com/api/teams/ac3me/cards/3f1c9b2e-… \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "phaseId": "ph8wm3xs" }'

Delete and restore

DELETE /teams/:team/cards/:id
POST   /teams/:team/cards/:id/restore

Delete is a soft delete. The app shows an undo for it, and restore is that undo as an endpoint.

Field value shapes

What to put in values, by field type:

Field type JSON value
text, long_text string
email, phone, url string, format-checked
number, currency number
date "YYYY-MM-DD"
datetime ISO 8601 string
select option id (from the field’s config.options)
multi_select array of option ids
checkbox boolean
assignee user id (from the members endpoint)

Attachment, reference and table fields are set in the app, and formula fields are computed and read-only. You can still read an attachment’s bytes over the API (see Attachments below).

Comments

GET    /teams/:team/cards/:id/comments
POST   /teams/:team/cards/:id/comments
PATCH  /teams/:team/comments/:id
DELETE /teams/:team/comments/:id

Post with { "body": "..." } (plain text, up to 10,000 characters). Editing and deleting are limited to the comment’s author.

Members

GET /teams/:team/members

Lists the team’s members with user id, name, email and role. Assignee field values take these user ids.

Attachments

GET /teams/:team/attachments/:id/download

An attachment field’s value is an array of file entries, each { id, name, size, mime, uploaded_at, uploaded_by }. Pass an entry’s id here to stream that file back, with Content-Type from the stored mime type and a Content-Disposition filename. Files live in internal storage and are streamed through the API, with no presigned link involved, so this request carries your token like any other. A deleted attachment returns 410.

Uploading happens in the app; this endpoint is read-only.

Webhooks

Pipes can push events to your endpoint instead of you polling. Webhooks are configured per pipe, in the app under the pipe’s settings or via the API (Standard plan and up, scope automations:write):

GET    /teams/:team/pipes/:pipe/webhooks
POST   /teams/:team/pipes/:pipe/webhooks
PATCH  /teams/:team/pipes/:pipe/webhooks/:id
DELETE /teams/:team/pipes/:pipe/webhooks/:id
POST   /teams/:team/pipes/:pipe/webhooks/:id/regenerate-secret
POST   /teams/:team/pipes/:pipe/webhooks/:id/test
GET    /teams/:team/pipes/:pipe/webhooks/:id/deliveries

Create one with:

{
  "url": "https://example.com/hooks/track40",
  "eventTypes": ["card.created", "card.moved", "comment.created"],
  "payloadFormat": "full"
}
  • eventTypes: which events to receive. The commonly useful ones are card.created, card.moved, card.updated, card.labels_changed, card.deleted, card.due_date_changed, comment.created, comment.edited and comment.deleted; any type you see in the app’s activity feed is accepted.
  • payloadFormat: full (the default) embeds the hydrated card (the shape above) under card, limited to the fields the webhook’s creator can see, so most receivers need no follow-up call. A delivery falls back to the envelope alone when the card cannot be embedded (deleted mid-flight, for example). thin delivers the event envelope only, so your endpoint receives ids and fetches what it needs; choose it when the receiving URL should never hold card content.

Each delivery is a POST of:

{
  "id": "…",
  "type": "card.moved",
  "teamId": "…",
  "pipeId": "…",
  "cardId": "…",
  "actorId": "…",
  "createdAt": "2026-08-27T04:30:11.000Z",
  "payload": { "from": "ph2qzk4n", "to": "ph8wm3xs" },
  "card": { "…": "present when payloadFormat is full" }
}

Every delivery carries these headers:

Header Value
X-Pipe-Signature sha256=<hex>, an HMAC-SHA256 of the raw body
X-Pipe-Event-Id The event id, identical across retries of one event
X-Pipe-Event-Type The event type, matching type in the body
X-Pipe-Attempt Which attempt this is, counting from 1

Verify the signature by recomputing HMAC-SHA256 over the raw request body with the webhook’s signing secret and comparing the result against the header.

Failed deliveries retry after one minute, five minutes, thirty minutes, four hours and a day, six attempts in all. One event can therefore reach you more than once, so key your idempotency on X-Pipe-Event-Id. It holds the same value as the body’s id and stays fixed across attempts. A webhook that keeps failing is marked broken and stops receiving until you re-enable it, and deliveries lists recent attempts with their response status for debugging.

test posts a fixed body of { "type": "test", "message": "…", "sentAt": "…" }, not an event envelope, so it carries no X-Pipe-Event-Id and no card whatever the payload format. It proves the URL and the signature work, and nothing more.

Stability

The endpoints on this page are the surface we support and keep stable; other routes you may notice the app calling are internal and can change without notice. For questions, gaps, or something you need that is not here, email

support@track40.com and a human will answer.

← All docs