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 anissuesarray. 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 idphaseId: move the card to another phaselabels: the card’s label ids (full replacement)dueDate: ISO datetime, ornullto 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 arecard.created,card.moved,card.updated,card.labels_changed,card.deleted,card.due_date_changed,comment.created,comment.editedandcomment.deleted; any type you see in the app’s activity feed is accepted.payloadFormat:full(the default) embeds the hydrated card (the shape above) undercard, 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).thindelivers 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.