REST API Reference
The Defiant public REST API provides HTTP access to projects, sprints, mandates, and inbox items. The TypeScript SDK and MCP server call this API internally.
Base URL
https://app.defiant.build/api/v1For self-hosted deployments using Supabase Edge Functions directly:
https://<your-project>.supabase.co/functions/v1/api-v1Authentication
All endpoints require a Defiant API key in the Authorization header:
Authorization: Bearer dft_your_api_key_hereGenerate API keys at Settings → API Keys in the Defiant web app. Keys are SHA-256 hashed before storage; the raw key is shown once at creation.
For local development:
export DEFIANT_API_KEY=dft_your_key# The SDK picks this up automaticallyLegacy: server-to-server calls previously used Supabase service keys:
Authorization: Bearer <supabase-service-role-key>Rate limiting
| Tier | Requests per minute |
|---|---|
| Authenticated user | 120 |
| Service role | 600 |
Exceeded limits return 429 Too Many Requests with a Retry-After header.
Error format
All errors follow this format:
{ "error": { "code": "SPRINT_NOT_FOUND", "message": "Sprint spr_01hw... not found", "details": null }}Common error codes:
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Invalid or expired JWT |
FORBIDDEN | 403 | Authenticated but lacks permission |
NOT_FOUND | 404 | Resource does not exist |
VALIDATION_ERROR | 422 | Request body failed validation |
CONDUCTOR_OFFLINE | 503 | The local Conductor is not running |
BUDGET_EXHAUSTED | 429 | Daily token budget exhausted |
MANDATE_VIOLATION | 409 | Request blocked by an active mandate |
Projects
GET /projects
List all projects for the authenticated user.
GET /projectsAuthorization: Bearer <jwt>{ "data": [ { "id": "proj_01hwabcdefg0000000000000000", "name": "my-app", "repo": "yourorg/my-app", "vertical": "b2b-saas", "branch": "main", "deployUrl": "https://myapp.com", "createdAt": "2026-05-01T10:00:00Z", "updatedAt": "2026-05-05T14:00:00Z" } ], "meta": { "total": 1 }}POST /projects
Create a new project.
POST /projectsAuthorization: Bearer <jwt>Content-Type: application/json
{ "name": "my-app", "repo": "yourorg/my-app", "vertical": "b2b-saas", "branch": "main", "deployUrl": "https://myapp.com"}GET /projects/:id
Get a single project.
PATCH /projects/:id
Update a project. Any field except id and createdAt can be updated.
DELETE /projects/:id
Delete a project. Does not delete the event log. Active sprints are cancelled first.
Sprints
GET /sprints
List sprints. Supports filtering and pagination.
GET /sprints?projectId=proj_01hw...&state=active&limit=20&offset=0Query parameters:
| Parameter | Type | Description |
|---|---|---|
projectId | string | Filter by project |
state | string | Filter by state: active, complete, failed, blocked |
limit | integer | Results per page (default: 20, max: 100) |
offset | integer | Pagination offset |
POST /sprints
Create a sprint.
POST /sprintsAuthorization: Bearer <jwt>Content-Type: application/json
{ "projectId": "proj_01hwabcdefg0000000000000000", "goal": "Add user profile page with avatar and activity feed", "priority": "normal", "tokenBudget": 200000}Response:
{ "data": { "id": "spr_01hwxyz0000000000000000", "projectId": "proj_01hwabcdefg0000000000000000", "goal": "Add user profile page with avatar and activity feed", "state": "INTAKE", "priority": "normal", "tokenBudget": 200000, "tokensUsed": 0, "agents": [], "prs": [], "createdAt": "2026-05-05T14:00:00Z", "updatedAt": "2026-05-05T14:00:00Z" }}GET /sprints/:id
Get a single sprint with full detail.
{ "data": { "id": "spr_01hwxyz0000000000000000", "state": "BUILD", "agents": ["captain", "architect", "builder"], "prs": [ { "number": 42, "url": "https://github.com/yourorg/my-app/pull/42", "state": "open" } ], "technicalPlan": { ... }, "tokensUsed": 47823, "elapsed": "00:12:34" }}DELETE /sprints/:id
Cancel a running sprint.
POST /sprints/:id/retry
Retry a failed or blocked sprint.
POST /sprints/:id/retryContent-Type: application/json
{ "fromState": "PLAN" // optional — defaults to last checkpoint}Events
GET /events
Query the event log.
GET /events?sprintId=spr_01hw...&type=state_transition&limit=50Query parameters:
| Parameter | Type | Description |
|---|---|---|
sprintId | string | Filter by sprint |
projectId | string | Filter by project |
type | string | Filter by event type |
agentId | string | Filter by agent |
since | ISO 8601 | Events after this timestamp |
before | ISO 8601 | Events before this timestamp |
limit | integer | Max results (default: 50, max: 500) |
Event types:
| Type | Description |
|---|---|
sprint.created | Sprint was created |
state.transition | Sprint state changed |
agent.dispatched | Agent was dispatched |
agent.completed | Agent issued completion certificate |
mandate.checked | Mandate check ran |
mandate.violated | Mandate violation found |
pr.opened | Pull request opened |
pr.merged | Pull request merged |
inbox.created | Inbox item created |
deploy.triggered | Deploy pipeline triggered |
deploy.succeeded | Deploy confirmed healthy |
GET /events/stream
Server-Sent Events stream for real-time event updates.
GET /events/stream?sprintId=spr_01hw...Accept: text/event-streamAuthorization: Bearer <jwt>data: {"type":"state.transition","sprintId":"spr_01hw...","from":"BUILD","to":"SHIP"}
data: {"type":"agent.dispatched","sprintId":"spr_01hw...","agent":"reviewer"}Inbox
GET /inbox
List inbox items.
GET /inbox?resolved=false&priority=criticalGET /inbox/:id
Get a single inbox item.
POST /inbox/:id/resolve
Resolve an inbox item.
POST /inbox/:id/resolveContent-Type: application/json
{ "response": "Use the existing users table for profile data"}Mandates
GET /mandates
List all mandates, optionally filtered by project (includes vertical pack mandates).
GET /mandates?projectId=proj_01hw...&category=securityGET /mandates/:id
Get a single mandate by ID (e.g., mandate_7).
POST /mandates/check
Run a mandate check against a code snapshot.
POST /mandates/checkContent-Type: application/json
{ "projectId": "proj_01hw...", "mandateId": "mandate_7", // optional — checks all if omitted "files": { "src/server.ts": "<file contents>" }}Response:
{ "data": { "passed": false, "violations": [ { "mandateId": "mandate_7", "ruleId": "no-raw-sql", "severity": "blocking", "file": "src/server.ts", "line": 47, "message": "Raw SQL detected. Use parameterized queries." } ] }}Webhooks
Defiant can POST events to a URL you configure. Register a webhook:
POST /webhooksContent-Type: application/json
{ "url": "https://myapp.com/defiant-webhook", "events": ["sprint.complete", "sprint.failed", "inbox.created"], "secret": "whsec_..." // used to sign payloads — verify with HMAC-SHA256}Payload format:
{ "id": "evt_01hw...", "type": "sprint.complete", "createdAt": "2026-05-05T14:38:22Z", "data": { "sprintId": "spr_01hw...", "projectId": "proj_01hw...", "state": "COMPLETE", "tokensUsed": 182304, "elapsed": "00:38:22" }}Signature verification (Node.js):
import { createHmac } from 'crypto';
function verifyWebhook(payload: string, signature: string, secret: string): boolean { const expected = createHmac('sha256', secret) .update(payload) .digest('hex'); return `sha256=${expected}` === signature;}
// In your webhook handler:const isValid = verifyWebhook( req.rawBody, req.headers['x-defiant-signature'], process.env.WEBHOOK_SECRET);