A small, stable HTTP API over the Ascanti data model — goals, projects, tasks, weeks, habits, time blocks, the daily Top three. Built for trusted first-party integrations (Cortex, email-brain, internal tooling).
https://ascanti.io/api/v1
Every request must carry an API key:
X-API-Key: ascanti_<48 hex chars>
Keys issued before October 2026 start with an older prefix; they keep working unchanged. Rotate to an ascanti_ key whenever convenient.
Generate keys in Settings → Integrations → API keys (you confirm with your password; up to 10 keys). Each key is tagged with a consumer label (e.g. cortex, email-brain); revoking one leaves the others working. Keys are stored as SHA-256 hashes — once shown, they cannot be recovered.
Resetting or changing a password keeps keys and webhooks working; the owner is emailed a list of them and can revoke any in Settings → Security → Connected apps. If the owner turned on extra protection after account recovery, a reset pauses them instead: requests answer 403 with {"code": "integration_paused"} and a Retry-After header. Keep the key, wait and retry later; it works again once the owner confirms it is them. Webhook events raised while paused are not sent, so catch up with /whats-new.
Send a descriptive User-Agent on every request:
User-Agent: my-integration/1.0 (+https://example.internal/my-integration)
Cloudflare's edge WAF historically blocked unidentified library-default UAs; sending a real one keeps your traffic identifiable in access logs and out of any future automated rules.
v1 is API-key-only — cookies and CSRF tokens are not used. The router ignores any Cookie header.
Every response includes:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 42 X-RateLimit-Reset: 37 # seconds until the window resets
Exceeding any limit returns 429 with a Retry-After header (in seconds). Wait at least that long before retrying.
List all goals for the authenticated user.
List all projects.
List tasks with filters. Returns {tasks: [...], total: N}. status is open (default), completed, or all.
Single task. :id is the local tt-... PK or the task's legacy task id (todoist_id: a local_t-... id, or a numeric id on older imported tasks).
Tasks with no project_id — the triage bucket. Same {tasks, total} shape as /tasks.
Goals, projects, and tasks modified since the given ISO 8601 timestamp. Response: {since, now, goals, projects, tasks}. Poll on a cadence and advance since to now from the response.
Current week (theme, top priorities, risks).
Specific week, e.g. 2026-W17.
The day's Top three items (stored as daily focus). Returns 404 when no focus exists for the date — distinguishes "nothing planned" from "planned but empty".
List habits.
List weekly commitments. weekId defaults to the current week.
List time blocks for a date (date required).
Create a goal. Body: {title, domain?, timeframe?, why?, successDefinition?}.
Partial update. Any subset of title, domain, timeframe, why, successDefinition/success_definition, status, category, starred, progressMethod. Use /goal-progress for percent updates.
Create a project. Body: {name (required), description?, area_id?, goal_id?, color?, status?}.
Partial update. Any subset of name, description, status, color, goal_id (null to unlink), area_id (null to unlink).
Create a task. Body: {content (required), description?, goal_id?, project_id?, due_at? OR due_string?, priority?, labels?}. priority accepts 1..4 or "P0".."P3"; due_string is parsed by chrono-node ("tomorrow 9am", "friday").
Partial update. Any subset of content, description, project_id, goal_id, due_at OR due_string, priority, labels.
Mark a task done. Returns {ok: true}.
Update goal progress and/or log evidence. Body: {goal_id (required), value? (0–100 absolute), delta? (signed), evidence_url?, note?}. At least one of value/delta/evidence_url/note is required.
Configure outbound webhooks in Settings → Integrations → Webhooks. Each webhook subscribes to one or more event names; deliveries are HMAC-signed and retry on failure.
task.created, task.updated, task.completedgoal.created, goal.updated, goal.completedproject.created, project.updated* — all events{
"event": "task.created",
"delivered_at": "2026-04-25T17:53:06Z",
"data": { /* normalized v1 entity payload */ }
}
X-Ascanti-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
The signed payload is ${unix}.${json_body}, HMAC-SHA256'd with your webhook's signing secret (returned once at registration). Reject deliveries where the signature doesn't verify or t is more than ±5 minutes from your server clock.
active=0 on the webhook.internal/.local| Status | Meaning |
|---|---|
| 400 | Malformed request — missing field, bad JSON, bad date, mutually exclusive fields together |
| 401 | Missing or invalid X-API-Key |
| 404 | Resource not found OR not owned by this key (intentionally indistinguishable) |
| 409 | Conflict — e.g. duplicate consumer label on key generation |
| 429 | Rate limit exceeded (per-key, per-tenant, or per-IP). Honour Retry-After |
| 500 | Server error — safe to retry with backoff |
All errors share the shape {"error": "human-readable message"}. The only 403 on v1 is {"code": "integration_paused"} (the account is paused after an account recovery: keep the key and retry after Retry-After). Otherwise either the key is valid and the resource is accessible, or it isn't.
curl -sfS \ -H "X-API-Key: $ASCANTI_API_KEY" \ -H "User-Agent: my-integration/0.1" \ https://ascanti.io/api/v1/goals
docs/api.md and docs/INTEGRATION_GUIDE.md in the Ascanti repo.