temporalBLOCK Guide
Every feature — from first API call to advanced Spiral integration — with working code examples for both novice and advanced users.
Authentication
Every request requires an X-API-Key header. Get your key from the dashboard. Keys follow the format tblk_(live|test)_…. The key goes in the X-API-Key header — not Authorization: Bearer. Sending it as a Bearer token returns a 401 with code WRONG_AUTH_HEADER. A missing or invalid key returns 401 INVALID_API_KEY.
// All requests — add this header
fetch("https://api.temporalblock.com/api/v1/calibrate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.TBLK_API_KEY, // never hardcode
},
body: JSON.stringify({ timezone: "America/New_York" }),
})| Tier | Price | Skills rows | Included calls / mo |
|---|---|---|---|
| Lite | $9 | read-only | calibrate + bridge only |
| Standard | $24 | 10 active | snippet / briefing included |
| Pro | $49 | 100 active | 1,500 snippet · 1,500 briefing · 150 deep |
| Enterprise | custom | unlimited | negotiated |
PAYG past included quota: snippet $0.02 · briefing $0.04 · deep $0.10.
Calibrate
POST /api/v1/calibrate returns a prompt block that anchors your LLM to the certified current date, time, and the boundary of its training data. Call this before every conversation that involves any time-sensitive question.
// Detect timezone in the browser or React Native
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// e.g. "America/New_York", "America/Chicago", "America/Los_Angeles",
// "America/Denver", "Europe/London", "Asia/Tokyo"
const { calibrateBlock } = await fetch(
"https://api.temporalblock.com/api/v1/calibrate",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.TBLK_API_KEY,
},
body: JSON.stringify({ timezone }),
}
).then((r) => r.json());
// Prepend calibrateBlock to your system prompt — it's a ready-made string
const systemPrompt = calibrateBlock + "\n\n" + yourExistingSystemPrompt;Additional options:
clientTime— user's self-reported local time (ISO string). Helps the block confirm against the server anchor.model— the LLM model name (e.g."claude-opus-4-5") so the block can name the correct knowledge boundary.- If the user's timezone is unknown, pass
timezone: "UTC"— the block labels it UTC so the LLM doesn't misrepresent it as local time.
Bridge & Full
Bridge reconciles your model's knowledge with the live web. POST /api/v1/bridge runs a real-time search on your behalf and returns a reconciliation block. POST /api/v1/full combines calibrate + bridge into one round trip — the most common choice.
POST /api/v1/full — recommended starting point
// /v1/full = calibrate + bridge in one call
// BYO provider key: pass your own Brave / Perplexity / OpenAI key
const result = await fetch("https://api.temporalblock.com/api/v1/full", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.TBLK_API_KEY,
},
body: JSON.stringify({
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
syncTier: "snippet", // "snippet" | "briefing" | "deep"
syncProvider: "perplexity", // or "brave" | "serpapi" | "openai" | "anthropic" | "google"
syncApiKey: process.env.PERPLEXITY_API_KEY,
}),
}).then((r) => r.json());
// result.calibrateBlock → prepend to system prompt
// result.bridgeBlock → append to system prompt (or inject as a user turn)
// result.meta → anchor confidence, Spiral coordinate, tier info| syncTier | Speed | Output | Min tier |
|---|---|---|---|
| snippet | fastest | Short factual summary | Standard |
| briefing | medium | Cited paragraph with sources | Standard |
| deep | slower | Parallel fan-out, merged result | Standard+ |
Managed current-events library — GET /v1/events
Query the stored temporal-event library without coupling your integration to one upstream dataset. The library can grow across sources while preserving source identity and attribution in record metadata and operational logs.
Constraints
- Pro or Enterprise plan
- API-key ownership is enforced
- Cursor-based pagination
- UTC and Spiral-window filters
Library records
- Canonical event time
- Spiral coordinate
- Source and retrieval metadata
- Stable pagination cursor
curl "$BASE_URL/api/v1/events?limit=25" \
-H "X-API-Key: $TEMPORALBLOCK_API_KEY"What is meta.spiralCoordinate?
Every /v1/full and /v1/bridge response includes a spiralCoordinate in the meta field. It is a compact representation of where you are in time across 12 overlapping cycles — millisecond through millennium — each expressed as a phase in [0, 1). At the novice level you can ignore it. Advanced users use it to trigger skills based on where the Spiral is, build circular time-aware UIs, or pass it as a Spiral anchor to other calls. Spiral section →
Free bridge + Spiral preview on /v1/calibrate
Want to see a bridge briefing before wiring up a provider key? Add includeBridgePreview: true and a bridgePreviewQuery to any /v1/calibrate call. The response includes meta.bridgePreview — a short cited briefing run on our key at no cost. Rate-limited; omitted (never a 4xx) when the quota is hit. Every calibrate response also carries a free Spiral preview in meta.spiral and meta.spiralCoordinate on all tiers with no extra flag. For production traffic use /v1/bridge or /v1/full with your own syncApiKey.
// Free bridge + Spiral preview on /v1/calibrate (all tiers, no extra charge)
const result = await fetch("https://api.temporalblock.com/api/v1/calibrate", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": process.env.TBLK_API_KEY },
body: JSON.stringify({
model: "gpt-4o",
includeBridgePreview: true,
bridgePreviewQuery: "What is the current state of quantum computing hardware?",
}),
}).then((r) => r.json());
// result.meta.bridgePreview → { query, briefing, citations, evidence }
// omitted (not a 4xx) when rate-limited or operator key unavailable
// result.meta.spiral → free basic Spiral preview, all tiers
// result.meta.spiralCoordinate → { scales, sin, cos } free on all tiersTools manifest for LLM agents
GET /api/v1/tools returns an OpenAI or Anthropic function-calling manifest of every API operation your key is entitled to invoke. Pass it directly to your LLM — the model then calls the right endpoints itself.
simple trigger documentation entirely.// Anthropic format — use ?format=anthropic so Claude sees short names
// ("calibrate", "create_skill") instead of the long internal identifiers
const { tools, baseUrl } = await fetch(
"https://api.temporalblock.com/api/v1/tools?format=anthropic",
{ headers: { "X-API-Key": process.env.TBLK_API_KEY } }
).then((r) => r.json());
// tools[n] → { name, description, input_schema, displayName }
// Pass directly to anthropic.messages.create({ tools, … })
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const msg = await anthropic.messages.create({
model: "claude-opus-4-5",
max_tokens: 1024,
tools,
messages: [{ role: "user", content: "What time is it for me?" }],
});// OpenAI format (default) — tools[n].function.name is the stable dispatch key
const { tools, baseUrl } = await fetch(
"https://api.temporalblock.com/api/v1/tools",
{ headers: { "X-API-Key": process.env.TBLK_API_KEY } }
).then((r) => r.json());
// Build a dispatch map once — key is function.name
const byName = Object.fromEntries(tools.map((t) => [t.function.name, t]));
// Execute one tool_call returned by the LLM
async function executeTool(name: string, args: Record<string, unknown>) {
const t = byName[name];
let path = t.path;
const body: Record<string, unknown> = {};
for (const [k, v] of Object.entries(args)) {
if (path.includes(`{${k}}`)) {
path = path.replace(`{${k}}`, String(v));
} else {
body[k] = v;
}
}
const isRead = t.method === "GET" || t.method === "DELETE";
return fetch(`${baseUrl}${path}`, {
method: t.method,
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.TBLK_API_KEY!,
},
body: isRead ? undefined : JSON.stringify(body),
}).then((r) => r.json());
}Tool name fields:
function.name— stable long-form (e.g.calibration__post_v1_calibrate). Use as the OpenAI dispatch map key.function.semanticName— terse snake_case (e.g.calibrate). Returned asnamein?format=anthropic.function.displayName— human label (e.g."Set Timer / Create Skill").
Timers, alarms & schedules (Skills)
POST /api/v1/skills creates a skill — a row with a trigger and a webhook URL. When the trigger fires, the API POSTs a signed payload to your endpoint. Use this for timers, alarms, reminders, and recurring schedules.
kind: "simple" for timers. The simple trigger is the recommended interface for bots and automated agents. Supply everyMs for a countdown or recurring timer, or atUtcMs for a one-shot alarm at a specific UTC instant. No Spiral knowledge needed.// 2-minute countdown timer
const { skill, webhookSecret } = await fetch(
"https://api.temporalblock.com/api/v1/skills",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.TBLK_API_KEY,
},
body: JSON.stringify({
label: "2 minute timer",
trigger: { kind: "simple", everyMs: 120_000 }, // 2 min = 120,000 ms
webhookUrl: "https://your-app.example.com/webhook",
}),
}
).then((r) => r.json());
// ⚠ webhookSecret is shown EXACTLY ONCE — save it now.
// It is the signing secret for verifying incoming webhook payloads.// Alarm at a specific UTC instant
trigger: { kind: "simple", atUtcMs: Date.now() + 30 * 60_000 } // 30 min from now
// Recurring — every hour, forever
trigger: { kind: "simple", everyMs: 3_600_000, repeat: true }
// Recurring with a cap — fire 12 times then stop
trigger: { kind: "simple", everyMs: 3_600_000, repeat: true, maxFires: 12 }
// Common everyMs values:
// 1 minute = 60_000
// 5 minutes = 300_000
// 2 hours = 7_200_000
// 1 day = 86_400_000// Verify an incoming webhook — same derivation for both signature headers
import { createHash, createHmac } from "node:crypto";
function verifyWebhook(req: Request, rawSecret: string): boolean {
const sigHeader = req.headers.get("X-Tblk-Signature") ?? "";
const [scheme, incoming] = sigHeader.split("=", 2);
if (scheme !== "sha256" || !incoming) return false;
const body = await req.text();
const signingKey = createHash("sha256").update(rawSecret).digest("hex");
const expected = createHmac("sha256", signingKey).update(body).digest("hex");
return expected === incoming;
// Also check X-Tblk-Account-Signature using your stable account key
}| trigger.kind | Required fields | Use case | Tier |
|---|---|---|---|
| simple | everyMs or atUtcMs | Timers, alarms, recurring — recommended for bots | Standard |
| at | atUtcMs | One-shot at exact UTC epoch ms | Standard |
| after | rung, fractionOfCycle, anchorUtcMs | Spiral-rung offset from an anchor | Standard |
| recur | rung, phase | Fire each time the Spiral hits a phase on a rung | Standard |
| displaceFrom | anchorUtcMs, op, thresholdMs | Fire when Spiral displacement exceeds threshold | Pro |
| whenStateMatches | stateKey, condition | Fire when a State KV value satisfies a predicate | Standard |
| whenPaceBelow | thresholdTokensPerSec, graceAfterProgress? | Fire when output pacing drops below threshold | Standard |
| whenPhaseRecurs | streamKey | Fire on novel recurrence in an event stream | Standard |
Retry schedule on non-2xx: 1 min → 5 min → 30 min → 2 h → 12 h (5 total). Return 2xx immediately — do not block on downstream processing.
Manual arm — a "press here to start" button
Creating a skill and starting its countdown are usually the same instant. Manual arm splits them: create the skill ahead of time, unarmed, then start the clock the moment a real person clicks a button in your own UI — not when your server happened to call the API. Reusable pattern: create a skill, render a chip Unarmed with a button, and flip it to Armed on click.
trigger.kind: "after". Set armPolicy: "manual" at create time. The skill comes back status: "unarmed" with a one-shot armToken — that token, not your X-API-Key, is what authorizes the arm call, so it's safe to send to the browser and wire straight to a button. POST /v1/skills/{id}/arm needs no API key at all. If nobody presses the button before armExpiresAtMs (default 15 min), the skill auto-cancels.1. Server-side: create the skill unarmed
// Your backend — needs your real API key. Do this ahead of the button render,
// e.g. as soon as the chat turn that offers "press here to start" is generated.
const { skill, webhookSecret, armToken, armExpiresAtMs } = await fetch(
"https://api.temporalblock.com/api/v1/skills",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.TBLK_API_KEY,
},
body: JSON.stringify({
label: "10 minute focus timer",
trigger: { kind: "after", rung: "min", fractionOfCycle: 10 / 60 },
webhookUrl: "https://your-app.example.com/webhook",
armPolicy: "manual",
}),
}
).then((r) => r.json());
// skill.status === "unarmed" — the 10-minute clock has NOT started yet.
// Send skill.id + armToken down to the browser. Never send X-API-Key to the browser.2. Browser: chip + button, wired to the arm endpoint
// Reusable vanilla-JS pattern — drop into any chat/UI framework.
// No API key here: armToken is the only credential this call needs.
async function armSkill(skillId, armToken, chipEl) {
chipEl.textContent = "Arming…";
const res = await fetch(
`https://api.temporalblock.com/api/v1/skills/${skillId}/arm`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ armToken }),
}
);
if (res.status === 200) {
chipEl.textContent = "Armed — running"; // countdown starts NOW
} else if (res.status === 410) {
chipEl.textContent = "Expired — create a new one";
} else if (res.status === 409) {
chipEl.textContent = "Already armed";
} else {
chipEl.textContent = "Could not arm — try again";
}
}
// <button onclick="armSkill(skillId, armToken, chip)">Start timer</button>
// <span id="chip">Unarmed</span>The arm call is meant for a human's own click — never have an LLM or agent call it on its own. Discovery tools (GET /v1/tools) do not list it for that reason; only armPolicy on create is agent-visible.
Spiral Block
The Temporal Spiral is a 12-rung coordinate system that maps any UTC instant to a phase in [0, 1) on every time scale from microseconds to millennia. GET /api/v1/spiral returns the live-anchored coordinate for the current moment.
// Live Spiral coordinate for right now
const coordinate = await fetch(
"https://api.temporalblock.com/api/v1/spiral",
{ headers: { "X-API-Key": process.env.TBLK_API_KEY } }
).then((r) => r.json());
// coordinate.scales → { us, ms, s, min, hr, day, wk, mo, yr, dec, cen, mil }
// Each scale: { rung, phase, sin, cos, source, cycleStartUtcMs, cycleEndUtcMs }
// coordinate.spiralReadable → human string, e.g. "2026-hr-0.648"
// Pin to a specific past or future moment with atUtcMs:
const pinned = await fetch(
"https://api.temporalblock.com/api/v1/spiral?atUtcMs=1780300000000",
{ headers: { "X-API-Key": process.env.TBLK_API_KEY } }
).then((r) => r.json());Common pitfall — don't feed anchored-now back in as atUtcMs
To encode "now", omit atUtcMs entirely — the server uses its own certified anchored clock. Fetching /v1/spiral/anchored-now (or reading any client clock) and passing the value back as atUtcMs marks the request synthetic: the server can't vouch for a round-tripped timestamp, so trust collapses to the 7 calendar-derived scales and sub-day rungs lose their anchor guarantee. Reserve atUtcMs for genuinely historical or synthetic instants. At runtime, a synthetic response carries meta.trustedScales (the scales still vouched for) and meta.trustNote explaining how to regain sub-day trust.
| Rung | Scale | Cycle length | Source |
|---|---|---|---|
| us | microsecond | 1 µs | lib-grounded |
| ms | millisecond | 1 ms | lib-grounded |
| s | second | 1 s | anchor |
| min | minute | 60 s | anchor |
| hr | hour | 3,600 s | anchor |
| day | day | 86,400 s | anchor |
| wk | week | 604,800 s | anchor |
| mo | month | calendar month | anchor |
| yr | year | 365.25 days | anchor |
| dec | decade | 10 years | anchor |
| cen | century | 100 years | anchor |
| mil | millennium | 1,000 years | anchor |
Sub-second rungs (us, ms) are library-grounded — derived from system clock without NTP correction. All other rungs are anchored to the certified NTP/NTS ensemble. Full Spiral docs →
Embed a Spiral Block in /v1/full Standard+
Pass includeSpiralBlock: true on /v1/full to receive a system-prompt-ready spiralBlock string alongside calibration and bridge output. Standard returns the scalar confidence; Pro adds the directional breakdown and sync-cycle phase. Lite keys receive a meta.spiral.upgradeReason hint instead and the block is omitted.
body: JSON.stringify({
includeSpiralBlock: true,
// ... other /v1/full fields
})
// result.spiralBlock → system-prompt-ready phase string (Standard+)
// result.meta.spiral.confidenceMs → scalar confidence in ms (Standard+)
// result.meta.spiral.confidenceBreakdown → { fromLastSyncMs, sampleDispersionMs, toNextSyncMs } (Pro+)
// result.meta.spiral.syncCyclePhase → { msSinceLastSync, msUntilNextSync, fractionThroughCycle } (Pro+)
// result.meta.spiral.upgradeReason → plain-language upsell hint (Lite only)State KV
A simple key-value store scoped to your API key. Store any JSON value under a string key. Use it as agent memory, or as the trigger condition for a whenStateMatches skill.
const BASE = "https://api.temporalblock.com/api/v1/state";
const headers = { "Content-Type": "application/json", "X-API-Key": process.env.TBLK_API_KEY };
// Write a value (Standard+)
await fetch(`${BASE}/session-mode`, {
method: "PUT",
headers,
body: JSON.stringify({ value: "focus" }),
});
// Read a value (all tiers)
const { value } = await fetch(`${BASE}/session-mode`, { headers }).then((r) => r.json());
// Delete
await fetch(`${BASE}/session-mode`, { method: "DELETE", headers });
// List all keys
const { keys } = await fetch(`${BASE}?prefix=session`, { headers }).then((r) => r.json());
// Trigger a skill when the value becomes "urgent":
trigger: {
kind: "whenStateMatches",
stateKey: "session-mode",
condition: { eq: "urgent" },
}Causal Block
Strict event ordering across every node — useful for distributed systems that need a single authoritative "happened-before" relationship regardless of network clock drift. Three endpoints: /now, /merge, and /compare.
const BASE = "https://api.temporalblock.com/api/v1/causal";
const H = { "Content-Type": "application/json", "X-API-Key": process.env.TBLK_API_KEY };
// Stamp the current moment — returns a causal clock vector
const { clock } = await fetch(`${BASE}/now`, { method: "POST", headers: H }).then(r => r.json());
// Merge two clocks from different nodes (returns the union — the "latest" happened-before)
const merged = await fetch(`${BASE}/merge`, {
method: "POST",
headers: H,
body: JSON.stringify({ clocks: [clockA, clockB] }),
}).then(r => r.json());
// Compare two clocks — returns "before" | "after" | "concurrent"
// (Pro+ only)
const { relation } = await fetch(`${BASE}/compare`, {
method: "POST",
headers: H,
body: JSON.stringify({ a: clockA, b: clockB }),
}).then(r => r.json());/now and /merge are trickle-limited on Lite. /compare requires Pro+. Full Causal Block docs →
Output Pacing
Track your model's token output rate against an authoritative Spiral-anchored clock. The API supplies elapsed time, rate, and estimated finish — it does not count tokens itself. Use it to detect when a model is running slow, stalled, or ahead of a budget.
const BASE = "https://api.temporalblock.com/api/v1/pacing";
const H = { "Content-Type": "application/json", "X-API-Key": process.env.TBLK_API_KEY };
// 1. Open a session at the start of the LLM response stream
const { session } = await fetch(`${BASE}/sessions`, {
method: "POST",
headers: H,
body: JSON.stringify({ model: "claude-opus-4-5", targetTokensPerSec: 40 }),
}).then(r => r.json());
const sessionId = session.id;
// 2. Send tick events as tokens arrive — include your own token count
await fetch(`${BASE}/sessions/${sessionId}/ticks`, {
method: "POST",
headers: H,
body: JSON.stringify({ tokensSinceLastTick: 32 }),
}).then(r => r.json());
// Response: { elapsedMs, tokensPerSec, estimatedFinishMs, spiralCoordinate, … }
// 3. Close the session when the stream ends
await fetch(`${BASE}/sessions/${sessionId}/close`, { method: "POST", headers: H });
// Stateless estimate — no session required (all tiers)
const estimate = await fetch(`${BASE}/estimate?tokens=500&model=claude-opus-4-5`, {
headers: H,
}).then(r => r.json());Live sessions require Standard+. The stateless /estimate endpoint is available on all tiers. Full Output Pacing docs →
Common pitfalls
① LLM reports the wrong local time
Cause: timezone was omitted from the calibrate call. The block contains UTC and the LLM reports it as the user's local time — which can be 5–12 hours off.
// ✗ Wrong — returns UTC, LLM will show the wrong hour
body: JSON.stringify({})
// ✓ Correct — always detect and forward the user's timezone
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
body: JSON.stringify({ timezone })
// If timezone is unknowable, pass "UTC" explicitly —
// the block labels it UTC so the LLM doesn't misrepresent it
body: JSON.stringify({ timezone: "UTC" })② LLM says "I can't create a timer"
Cause: the tool manifest was fetched before June 2026, or the LLM's tool context doesn't include the skills tool at all. The create_skill tool (semantic name) creates timers — but only if the description in the manifest uses the words "timer" and "alarm". Refresh the manifest.
// Refresh at bot startup — not at build time
const { tools } = await fetch(
"https://api.temporalblock.com/api/v1/tools?format=anthropic",
{ headers: { "X-API-Key": process.env.TBLK_API_KEY } }
).then((r) => r.json());
// The June 2026 manifest says "Set a timer, alarm, reminder…" in the description.
// A cached pre-June manifest may not.③ Timer request returns 400 LEGACY_TRIGGER_REJECTED
Cause: durationMs, fireAtUtcMs, rrule, and similar fields are pre-v7 and are explicitly rejected. Use kind: "simple" with everyMs instead.
// ✗ Wrong — durationMs is a rejected legacy field
trigger: { kind: "after", durationMs: 120000 }
// ✓ Correct — simple trigger with everyMs
trigger: { kind: "simple", everyMs: 120000 }④ Webhook secret not saved
Cause: the webhookSecret field in the skill creation response is shown exactly once and cannot be retrieved again. Save it immediately to your secrets store. If lost, delete the skill and recreate it.
⑤ Calibrate or bridge results cached
Cause: caching the calibrate response by timezone means every user in that zone gets a stale clock for the TTL duration — including the wrong minute and hour. Calibrate is cheap and must be called fresh per conversation. Bridge should likewise not be cached — the live search result is the point.