Skip to content

Reference · v1

Tudo API

REST endpoints for items + outbound webhooks for events. Authenticate with a personal access token; everything below works from cURL, any HTTP client, or our future SDK.

Authentication

Every request authenticates with a personal access token. Mint one in Settings → API tokens. Send it on every request:

bash
Authorization: Bearer tudo_•••••••••••••••••••••••••••••••••

Tokens inherit the minting user's tenant + role. They show their full value once at creation; lost tokens must be revoked and re-issued. Tokens optionally expire (30d / 90d / 1y).

Auth check

bash
curl https://app.usetudo.com/api/v1/me \
  -H "Authorization: Bearer $TUDO_TOKEN"
json
{
  "auth": { "source": "token", "scopes": ["*"] },
  "user":   { "id": "...", "email": "you@example.com", "full_name": "Felipe" },
  "tenant": { "id": "...", "name": "GreenBoost", "slug": "greenboost" }
}

Items

Items are rows on a board. Use these endpoints to read or mutate them from outside the UI. Every endpoint is tenant-scoped via the token.

GET/api/v1/items?board_id={uuid}&limit=&cursor=

Lists items in a single board. Returns up to limit rows (default 50, max 200) with a keyset cursor. Pass cursor from pagination.next_cursor to walk the next page.

bash
curl "https://app.usetudo.com/api/v1/items?board_id=$BOARD&limit=20" \
  -H "Authorization: Bearer $TUDO_TOKEN"
GET/api/v1/items/{id}

Returns the row plus every cell value keyed by column_id in data.column_values.

bash
curl https://app.usetudo.com/api/v1/items/$ITEM_ID \
  -H "Authorization: Bearer $TUDO_TOKEN"
POST/api/v1/items

Creates a row at the bottom of group_id (or the board's first group if omitted). Optional initial cell values go through the same path the UI uses, so any board automation fires.

bash
curl https://app.usetudo.com/api/v1/items \
  -H "Authorization: Bearer $TUDO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "board_id": "...",
    "name": "Order #1042",
    "column_values": [
      { "column_id": "...", "value": { "text": "Acme Co" } },
      { "column_id": "...", "value": { "option_id": "in-progress" } }
    ]
  }'
PATCH/api/v1/items/{id}

Renames the item and / or upserts cell values. Invalid column_ids are silently dropped; the response echoes applied_column_ids so you can tell.

bash
curl -X PATCH https://app.usetudo.com/api/v1/items/$ITEM_ID \
  -H "Authorization: Bearer $TUDO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order #1042 (rush)",
    "column_values": [
      { "column_id": "...", "value": { "option_id": "urgent" } }
    ]
  }'
DELETE/api/v1/items/{id}

Soft-deletes the row (lands in trash, restorable for 30 days). Fires the item.deleted webhook.

bash
curl -X DELETE https://app.usetudo.com/api/v1/items/$ITEM_ID \
  -H "Authorization: Bearer $TUDO_TOKEN"

Webhooks

Tudo POSTs to a URL you register in Settings → Webhooks whenever a subscribed event fires. Payloads are signed; reject any request whose signature doesn't match.

Envelope

json
{
  "id": "evt_4f3a8e2c…",
  "event": "approval.requested",
  "timestamp": 1714234567890,
  "tenant_id": "5b2…",
  "data": { /* event-specific payload */ }
}

Headers

  • X-Tudo-Event — same as event in the body.
  • X-Tudo-Tenant-Id — issuing tenant.
  • X-Tudo-Signaturet=<ms>,v1=<hex> where v1 is HMAC_SHA256(secret, "{t}.{rawBody}").
  • User-AgentTudo-Webhooks/1.

Verification (Node)

ts
import { createHmac } from "node:crypto";

export function verifyTudoWebhook(
  rawBody: string,
  signatureHeader: string,
  secret: string,
  toleranceSeconds = 300,
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=") as [string, string]),
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!t || !v1) return false;
  // Reject replays older than the tolerance window.
  if (Math.abs(Date.now() - t) > toleranceSeconds * 1000) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(v1, expected);
}

function timingSafeEqual(a: string, b: string) {
  if (a.length !== b.length) return false;
  let r = 0;
  for (let i = 0; i < a.length; i++) r |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return r === 0;
}

Verification (Python)

python
import hmac, hashlib, time

def verify_tudo_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t, v1 = int(parts.get("t", 0)), parts.get("v1", "")
    if not t or not v1: return False
    if abs(time.time() * 1000 - t) > tolerance * 1000: return False
    mac = hmac.new(secret.encode(), f"{t}.{raw_body.decode()}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, v1)

Event types

  • item.created { item_id, board_id, group_id, name, created_by, created_at }
  • item.deleted { item_id, board_id, group_id, name, deleted_at }
  • approval.requested { request_id, item_id, item_name, item_url, requested_by, requester_name, reason, mode, approver_ids }
  • approval.decided { request_id, step_id, item_id, item_name, item_url, requested_by, decided_by, decision, final_status, comment }

Subscribe an endpoint to * in the settings UI to opt into every event. New types roll out monthly; subscribing to * means new events deliver automatically.

Errors

Errors return JSON of the form { "error": "<CODE>", "message": "<human readable>" } with the matching HTTP status:

  • 401 UNAUTHENTICATED — missing / revoked / expired token.
  • 400 BAD_REQUEST — payload missing a required field or referencing a row outside the token's tenant.
  • 404 NOT_FOUND — id valid in shape, no row in scope.
  • 500 INTERNAL — database error; the message is safe to log.