Skip to content

Caching & conditional requests

Every GET response from the Bordio API carries an ETag. By sending it back on your next request you can ask “has this changed?” and get a tiny 304 Not Modified instead of the full body when nothing has changed.

Most data you read — task lists, definitions, custom fields — changes far less often than you poll it. Without caching, every GET re-downloads an identical body. Conditional requests let you:

  • Save bandwidth — an unchanged resource comes back as an empty 304 instead of the full payload.
  • Revalidate cheaply — one round-trip confirms your cached copy is still current.
  • Simplify cache logic — the server tells you when to refresh; you never guess.

On every GET, the API sets two headers:

ETag: "e3b0c44298fc1c149afbf4c8996fb924"
Cache-Control: private, no-cache
  • The ETag is a strong validator computed over the exact response body — if the body changes in any way, the ETag changes.
  • Cache-Control: private, no-cache means: you may store the response, but you must revalidate it with the server before reusing it (don’t serve it blind), and it is private to this key — never cache it in a shared/CDN cache.

This applies to all GET endpoints — tasks, subtasks, events, projects, members and every definitions resource (task statuses, types, custom fields, tags…).

  1. On a GET, store the response body together with its ETag.
  2. On your next GET of the same resource, send the stored value in an If-None-Match header.
  3. The server compares it to the current ETag:
    • Match304 Not Modified with an empty body. Reuse your cached copy.
    • No match200 OK with the new body and a new ETag. Replace your cached copy and stored ETag.
Terminal window
# First request — note the ETag
curl -i "https://api.bordio.com/public/v1/custom_fields?project_id=proj_6594a1b2c3d4e5f6a7b8c9d0" \
-H "Authorization: Bearer brd_sk_live_..."
# → 200 OK
# ETag: "e3b0c44298fc1c149afbf4c8996fb924"
# Later — revalidate with the stored ETag
curl -i "https://api.bordio.com/public/v1/custom_fields?project_id=proj_6594a1b2c3d4e5f6a7b8c9d0" \
-H "Authorization: Bearer brd_sk_live_..." \
-H 'If-None-Match: "e3b0c44298fc1c149afbf4c8996fb924"'
# → 304 Not Modified (no body — your cached copy is still current)

A minimal client cache:

const cache = new Map(); // url -> { etag, body }
async function getCached(url, apiKey) {
const prev = cache.get(url);
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
...(prev ? { 'If-None-Match': prev.etag } : {}),
},
});
if (res.status === 304) return prev.body; // unchanged — reuse cache
const body = await res.json();
cache.set(url, { etag: res.headers.get('ETag'), body });
return body;
}

If-None-Match is compared using the weak comparison function (RFC 7232 §3.2), so revalidation is forgiving about the weak-validator prefix:

  • A W/ prefix on either side is ignored — W/"abc" and "abc" are treated as the same tag.
  • You may send a comma-separated list of tags; a match on any one satisfies the condition: If-None-Match: "abc", W/"def".
  • You may send *, which matches any current representation.

You normally just echo back the exact ETag string you received — the weak comparison simply means a W/-prefixed tag from your HTTP client still matches.

  • Treat the ETag as opaque. It’s a hash — don’t parse it, compare only for equality, store it verbatim.
  • A 304 still counts as a request against your rate limit and usage. Conditional requests save bandwidth, not your request budget — so keep a sensible polling interval.
  • ETag is per representation. Different query parameters (e.g. a different project_id, page cursor, or filter) produce different bodies and therefore different ETags. Cache them per full URL.
  • Conditional requests apply to reads only (GET). Writes (POST/PATCH/DELETE) are never conditional here — for safe retries of writes, use idempotency keys instead.