API Reference
REST API endpoint documentation
All API requests must include the API Key in the Authorization header:
Authorization: Bearer sk_live_...
Base URL: https://api.surflet.app (production)
Endpoint Overview
| Method | Path | Description |
|---|---|---|
| POST | /v1/publish | Publish in one step |
| POST | /v1/pages | Create a draft |
| GET | /v1/pages/:id | Get a page |
| PATCH | /v1/pages/:id | Update a page |
| POST | /v1/pages/:id/publish | Publish a draft |
| POST | /v1/pages/:id/revoke | Revoke a page |
| POST | /v1/pages/:id/renew | Renew an expired page |
| GET | /v1/pages/:id/approval | Get approval status |
| POST | /v1/pages/:id/actions/:actionId | Execute an action |
| GET | /v1/pages/:id/audit | Get audit log |
| GET | /v1/inbox | List inbox entries |
| PATCH | /v1/inbox/:id/read | Mark as read |
| PATCH | /v1/inbox/:id/star | Star/unstar |
| DELETE | /v1/inbox/:id | Archive entry |
| GET | /v1/tenant | Get tenant info |
| GET | /v1/tenant/dashboard | Dashboard metrics |
| GET | /v1/audit/verify | Verify audit chain integrity |
| POST | /v1/workflows | Create a workflow |
| GET | /v1/workflows/:groupId | Get workflow status |
| PATCH | /v1/pages/:id/blocks | Append blocks (streaming publish) |
| GET | /v1/pages/:id/analytics | Get page analytics |
| GET | /v1/quality-score | Agent quality score |
| GET | /v1/templates | List templates |
| POST | /v1/publish/template/:name | Publish from template |
| POST | /v1/pages/:id/comments | Add a comment |
| GET | /v1/pages/:id/comments | Get comments |
| PATCH | /v1/pages/:id/comments/:commentId | Edit a comment |
| DELETE | /v1/pages/:id/comments/:commentId | Delete a comment |
| POST | /v1/pages/:id/files/presign | Request presigned upload URL |
| POST | /v1/pages/:id/files/complete | Commit a completed upload |
| GET | /v1/pages/:id/files | Get file list |
| GET | /v1/pages/:id/files/:fileId/download | Download a file |
| DELETE | /v1/pages/:id/files/:fileId | Delete a file |
| POST | /v1/pages/:id/associations | Create a page association |
| GET | /v1/pages/:id/events | SSE real-time event stream |
| GET | /v1/pages/:id/comments/stream | SSE comment stream |
| PATCH | /v1/pages/:id/blocks/:blockId | Fix an individual block |
| PUT | /v1/pages/:id/blocks/reorder | Reorder blocks |
| POST | /v1/pages/:id/blocks/insert | Insert a block at a position |
| POST | /v1/publish/a2ui | A2UI protocol publish |
| POST | /v1/publish/ag-ui | AG-UI protocol publish |
| GET | /v1/agent/inbox | Agent event inbox |
| GET | /v1/agent/events | Agent events SSE stream |
| POST | /v1/events/:id/ack | Acknowledge an event |
| GET | /v1/agent/inbox/count | Pending event count |
| GET | /v1/pages/:id/agent-events | Page event list |
| POST | /v1/auth/oauth/github | Initiate GitHub OAuth |
| POST | /v1/auth/oauth/google | Initiate Google OAuth |
| GET | /v1/auth/me | Get current session |
| POST | /v1/auth/logout | Log out |
| POST | /v1/auth/magic-link/send | Send magic link |
| GET | /v1/auth/magic-link/verify | Verify magic link token |
| POST | /v1/auth/agent/register | Agent self-registration |
| POST | /v1/auth/claim | Claim agent account |
| GET | /v1/account/api-keys | List API keys |
| POST | /v1/account/api-keys | Create API key |
| DELETE | /v1/account/api-keys/:id | Revoke API key |
POST /v1/publish
Create and publish a page in one step. Equivalent to POST /v1/pages + POST /v1/pages/:id/publish.
The response includes a warnings array (if any blocks have data issues):
{
"pageId": "page_xxx",
"pageUrl": "https://...",
"warnings": [
"Block 'b1' (callout): use 'content' not 'message'. Fix: rename data.message → data.content",
"Block 'b2' (metric): data.items is missing. Expected: { items: [{ label, value }] }"
]
}
The page still publishes successfully — warnings are advisory. Agents can fix problematic blocks with PATCH /v1/pages/:id/blocks/:blockId.
Request body:
{
"version": "1.0",
"page_type": "approval",
"title": "Refund Approval #4821",
"summary": "Customer reported product damage, recommend full refund",
"priority": "high",
"tags": ["refund", "cs"],
"blocks": [
{
"type": "key_value",
"data": {
"title": "Ticket Overview",
"pairs": [
{"key": "Customer", "value": "Alice Wang"},
{"key": "Amount", "value": "$127.50"}
]
}
}
],
"actions": [
{
"action_id": "act_approve",
"label": "Approve Refund",
"style": "primary"
}
],
"access": {
"mode": "authenticated",
"allowed_identities": [
{"type": "email", "value": "[email protected]"}
]
},
"notify": {
"channels": [
{"type": "slack", "target": "#cs-approvals"}
]
},
"on_action": {
"default_url": "https://agent.example.com/webhook"
}
}
Response (201):
{
"pageId": "pg_abc123",
"pageUrl": "https://hai.surf/p/pg_abc123",
"shortUrl": "https://srf.lt/abc123",
"status": "active",
"createdAt": "2026-03-23T15:00:00Z",
"expiresAt": "2026-03-30T15:00:00Z"
}
POST /v1/pages
Create a draft page without publishing immediately.
Request body: Same as /v1/publish.
Response (201):
{
"pageId": "pg_abc123",
"status": "draft",
"createdAt": "2026-03-23T15:00:00Z"
}
GET /v1/pages/:id
Get page details.
Response (200):
{
"pageId": "pg_abc123",
"pageUrl": "https://hai.surf/p/pg_abc123",
"status": "active",
"page_type": "approval",
"title": "Refund Approval #4821",
"summary": "...",
"priority": "high",
"tags": ["refund"],
"created_by": "agent:refund-bot",
"created_at": "2026-03-23T15:00:00Z",
"blocks": ["..."],
"actions": ["..."],
"access": {"...": "..."},
"approval_chain": {"...": "..."},
"stats": {
"view_count": 3,
"action_count": 1,
"last_viewed_at": "2026-03-23T16:00:00Z"
}
}
PATCH /v1/pages/:id
Update a page. Only pages in draft or active status can be updated.
Request body: Fields to update (partial update).
{
"title": "Refund Approval #4821 (Updated)",
"blocks": ["..."]
}
Response (200): The full updated page object.
POST /v1/pages/:id/publish
Publish a draft page.
Request body: Empty, or optional override parameters.
{
"notify": {
"channels": [
{"type": "slack", "target": "#approvals"}
]
}
}
Response (200):
{
"pageId": "pg_abc123",
"pageUrl": "https://hai.surf/p/pg_abc123",
"status": "active",
"publishedAt": "2026-03-23T15:00:00Z"
}
POST /v1/pages/:id/revoke
Revoke a published page. Once revoked, the page is no longer accessible.
Request body:
{
"reason": "Duplicate request"
}
Response (200):
{
"pageId": "pg_abc123",
"status": "revoked",
"revokedAt": "2026-03-23T16:00:00Z"
}
POST /v1/pages/:id/renew
Renew an expired page.
Request body:
{
"expires_at": "2026-05-01T00:00:00Z"
}
Response (200):
{
"pageId": "pg_abc123",
"status": "active",
"expiresAt": "2026-05-01T00:00:00Z"
}
GET /v1/pages/:id/approval
Get approval chain status.
Response (200):
{
"chain_status": "in_progress",
"mode": "sequential",
"current_step": "step_finance",
"steps": [
{
"step_id": "step_manager",
"name": "Manager Approval",
"status": "completed",
"decision": "approved",
"decided_by": "[email protected]",
"decided_at": "2026-03-23T10:30:00Z"
},
{
"step_id": "step_finance",
"name": "Finance Confirmation",
"status": "pending",
"assignees": ["[email protected]", "[email protected]"],
"decisions": []
}
]
}
POST /v1/pages/:id/actions/:actionId
Execute a page action (such as approve or reject).
Request body:
{
"actor": {
"identity": "[email protected]",
"auth_token": "..."
},
"comment": "Approved, looks good",
"form_data": null
}
Response (200):
{
"action_id": "act_approve",
"status": "executed",
"page_state": {
"previous": "active",
"current": "completed"
},
"executedAt": "2026-03-23T15:30:00Z"
}
GET /v1/pages/:id/audit
Get the audit log for a page.
Query parameters:
| Parameter | Description | Default |
|---|---|---|
page | Page number | 1 |
per_page | Items per page | 50 |
Response (200):
{
"items": [
{
"event_id": "aud_001",
"event_type": "page.created",
"timestamp": "2026-03-23T15:00:00Z",
"actor": "agent:refund-bot",
"details": {"...": "..."},
"chain_hash": "sha256:abc123..."
},
{
"event_id": "aud_002",
"event_type": "page.published",
"timestamp": "2026-03-23T15:00:01Z",
"actor": "system",
"chain_hash": "sha256:def456..."
}
],
"total": 5,
"page": 1,
"per_page": 50
}
GET /v1/inbox
List inbox entries for the authenticated user. Requires authentication.
Query parameters:
| Parameter | Description |
|---|---|
status | Filter by status: unread, read, or archived |
priority | Filter by priority level |
pageType | Filter by page type |
limit | Maximum number of entries to return |
offset | Pagination offset |
Response (200):
{
"entries": [
{
"id": "inbox_abc123",
"pageId": "pg_abc123",
"title": "Refund Approval #4821",
"pageType": "approval",
"status": "unread",
"starred": false,
"priority": "high",
"createdAt": "2026-03-23T15:00:00Z"
}
],
"total": 12
}
PATCH /v1/inbox/:id/read
Mark an inbox entry as read. Requires authentication.
curl -X PATCH https://api.surflet.app/v1/inbox/inbox_abc123/read \
-H "Authorization: Bearer sk_live_..."
Response: 204 No Content.
PATCH /v1/inbox/:id/star
Star or unstar an inbox entry. Requires authentication.
Request body:
{
"starred": true
}
Omit starred to default to true. Set false to unstar.
Response: 204 No Content.
DELETE /v1/inbox/:id
Archive an inbox entry (soft delete). Requires authentication.
curl -X DELETE https://api.surflet.app/v1/inbox/inbox_abc123 \
-H "Authorization: Bearer sk_live_..."
Response: 204 No Content.
GET /v1/tenant
Get current tenant information.
Response (200):
{
"tenant_id": "tn_xxx",
"name": "My Company",
"plan": "pro",
"usage": {
"pages_this_month": 150,
"pages_limit": 1000
}
}
GET /v1/tenant/dashboard
Get dashboard metrics.
Response (200):
{
"period": "last_30_days",
"metrics": {
"total_pages": 150,
"active_pages": 23,
"completed_pages": 112,
"avg_resolution_time_hours": 4.2,
"approval_rate": 0.89
}
}
GET /v1/audit/verify
Verify audit chain integrity.
Response (200):
{
"status": "valid",
"chain_length": 1523,
"first_hash": "sha256:000...",
"last_hash": "sha256:fff...",
"verified_at": "2026-03-23T16:00:00Z"
}
POST /v1/workflows
Create a workflow (multi-page orchestration).
Request body:
{
"name": "Refund Processing",
"pages": [
{"...": "page 1 payload"},
{"...": "page 2 payload"}
],
"flow": "sequential"
}
Response (201):
{
"group_id": "wf_abc123",
"pages": ["pg_001", "pg_002"],
"status": "active"
}
GET /v1/workflows/:groupId
Get workflow status.
Response (200):
{
"group_id": "wf_abc123",
"name": "Refund Processing",
"status": "in_progress",
"pages": [
{"pageId": "pg_001", "status": "completed"},
{"pageId": "pg_002", "status": "active"}
]
}
PATCH /v1/pages/:id/blocks
Append content blocks to an existing page (streaming / progressive publish). The page status must be active or draft.
Request body:
{
"blocks": [
{
"type": "text",
"data": {"title": "New Analysis", "content": "## Findings\n\nAfter deeper analysis..."}
},
{
"type": "chart",
"data": {"title": "Trend", "chart_type": "line", "data": {"labels": ["Mon","Tue"], "datasets": [{"label": "DAU", "data": [1200, 1500]}]}}
}
]
}
Response (200):
{
"pageId": "pg_abc123",
"version": 3,
"totalBlocks": 8,
"appendedBlocks": 2,
"appendedBlockIds": ["b_6_a1b2c3d4", "b_7_e5f6g7h8"]
}
GET /v1/pages/:id/analytics
Get page view analytics.
Response (200):
{
"pageId": "pg_abc123",
"totalViews": 47,
"uniqueViewers": 12,
"dailyViews": {
"2026-03-20": 15,
"2026-03-21": 18,
"2026-03-22": 8
},
"createdAt": "2026-03-20T10:00:00Z",
"status": "active"
}
GET /v1/quality-score
Get the agent quality score for the current tenant.
Response (200):
{
"tenantId": "tn_xxx",
"totalPages": 87,
"totalDecisions": 72,
"acceptanceRate": 89,
"avgDecisionTimeMinutes": 35,
"score": 78,
"period": "last_100_pages"
}
GET /v1/templates
List all available page templates.
Response (200):
{
"templates": [
{
"name": "refund_approval",
"description": "Customer refund approval page...",
"variables": [
{"key": "customer", "label": "Customer name", "required": true, "type": "string"},
{"key": "amount", "label": "Refund amount", "required": true, "type": "number"}
]
}
]
}
POST /v1/publish/template/:name
Publish a page from a preset template. The template name is a path parameter; variables are passed in the request body.
Request body:
{
"variables": {
"customer": "Alice Wang",
"order_id": "#ORD-7891",
"amount": 127.50,
"reason": "Product arrived damaged"
}
}
Response (201): Same as POST /v1/publish.
Error (404): Returns the list of available templates when the template does not exist.
POST /v1/pages/:id/comments
Add a comment to a page. Supports nested thread replies.
Request body:
{
"content": "The refund amount needs to be verified.",
"author_name": "Manager Zhang",
"author_type": "user",
"block_id": "blk_details",
"parent_id": "cmt_abc123"
}
| Field | Type | Description |
|---|---|---|
content | string | Comment content (required) |
author_name | string | Author display name (optional) |
author_type | string | Author type: user / agent (optional, defaults to user) |
block_id | string | Associated Block ID (optional, for anchoring the comment) |
parent_id | string | Parent comment ID (optional, for nested replies) |
Response (201): The created comment object.
GET /v1/pages/:id/comments
Get all comments for a page. No authentication required (accessible on public pages).
Response (200):
{
"comments": [
{
"id": "cmt_abc123",
"pageId": "pg_abc123",
"content": "The refund amount needs to be verified.",
"authorName": "Manager Zhang",
"authorType": "user",
"createdAt": "2026-03-23T15:00:00Z"
}
]
}
PATCH /v1/pages/:id/comments/:commentId
Edit comment content.
Request body:
{
"content": "Amount confirmed as correct.",
"editor_identity": "[email protected]"
}
Response (200): The updated comment object.
DELETE /v1/pages/:id/comments/:commentId
Delete a comment.
Request body (optional):
{
"deleter_identity": "[email protected]"
}
Response: 204 No Content.
POST /v1/pages/:id/files/presign
Request a presigned upload URL. Authentication is optional — public pages allow anonymous uploads.
Request body:
{
"filename": "invoice-4821.pdf",
"mime_type": "application/pdf",
"size_bytes": 524288,
"uploaded_by": "agent:refund-bot",
"upload_type": "agent"
}
Response (200):
{
"uploadUrl": "/v1/storage/upload/tenant%2Fpg_abc123%2F1713000000000_invoice-4821.pdf",
"storageKey": "tenant/pg_abc123/1713000000000_invoice-4821.pdf",
"filename": "invoice-4821.pdf",
"mimeType": "application/pdf",
"uploadedBy": "agent:refund-bot",
"uploadType": "agent"
}
After receiving the uploadUrl, PUT the file bytes directly to it (no JSON, raw body). Then call /complete to commit the record.
POST /v1/pages/:id/files/complete
Commit an upload after the file bytes have been PUT to the uploadUrl. Registers the file record and publishes a file.uploaded SSE event.
Request body:
{
"storage_key": "tenant/pg_abc123/1713000000000_invoice-4821.pdf",
"filename": "invoice-4821.pdf",
"mime_type": "application/pdf",
"uploaded_by": "agent:refund-bot",
"upload_type": "agent"
}
Response (201):
{
"id": "file_abc123",
"pageId": "pg_abc123",
"filename": "invoice-4821.pdf",
"mimeType": "application/pdf",
"sizeBytes": 524288,
"storageKey": "tenant/pg_abc123/1713000000000_invoice-4821.pdf",
"uploadedBy": "agent:refund-bot",
"createdAt": "2026-03-23T15:00:00Z"
}
See File Uploads guide for the complete three-step flow.
GET /v1/pages/:id/files
Get all file attachments for a page. No authentication required.
Response (200):
{
"files": [
{
"id": "file_abc123",
"filename": "invoice-4821.pdf",
"mimeType": "application/pdf",
"sizeBytes": 524288,
"uploadedBy": "agent:refund-bot",
"createdAt": "2026-03-23T15:00:00Z"
}
]
}
GET /v1/pages/:id/files/:fileId/download
Download a file. Redirects to the presigned download URL which serves the raw file bytes.
Use -L in curl or follow redirects in your HTTP client.
Error (404): File not found or does not belong to the specified page.
DELETE /v1/pages/:id/files/:fileId
Delete a file. Removes the database record and the file from disk. Publishes a file.deleted SSE event.
Response: 204 No Content.
POST /v1/pages/:id/associations
Create a typed association between two pages. Requires authentication.
Request body:
{
"targetPageId": "pg_xyz789",
"relationType": "follow_up"
}
| Field | Type | Description |
|---|---|---|
targetPageId | string | The page to associate with |
relationType | string | Association type (e.g. follow_up, duplicate, related) |
Response (201):
{
"id": "assoc_abc123",
"sourcePageId": "pg_abc123",
"targetPageId": "pg_xyz789",
"relationType": "follow_up",
"tenantId": "tenant_xxx",
"createdAt": "2026-04-15T10:00:00Z"
}
GET /v1/pages/:id/events
SSE (Server-Sent Events) real-time event stream. Long-lived connection for receiving real-time page updates.
Response headers:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Event format:
data: {"type":"connected","pageId":"pg_abc123"}
data: {"type":"blocks.appended","data":{"version":3,"newBlocks":[...]}}
data: {"type":"action.executed","data":{"actionId":"act_approve","actor":"..."}}
data: {"type":"comment.added","data":{"commentId":"cmt_abc","content":"..."}}
data: {"type":"file.uploaded","data":{"fileId":"file_abc","filename":"..."}}
The server sends a keepalive comment every 30 seconds to maintain the connection.
GET /v1/pages/:id/comments/stream
SSE stream for comment events on a specific page. No authentication required (works on public pages).
Response headers:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
The server sends an initial : heartbeat line immediately after connection. Subsequent messages are comment-related page events from the same Redis channel as /events.
curl -N https://api.surflet.app/v1/pages/pg_abc123/comments/stream
Real-Time Streaming (SSE)
Surflet exposes three SSE streams for different audiences:
| Endpoint | Auth | Scope | Purpose |
|---|---|---|---|
GET /v1/pages/:id/events | No | Per page | All page events (blocks, actions, comments, files) |
GET /v1/pages/:id/comments/stream | No | Per page | Comment events (subset of page events) |
GET /v1/agent/events | Yes | Per tenant | Agent-targeted events across all pages |
Connection pattern
const es = new EventSource(
"https://api.surflet.app/v1/pages/pg_abc123/events"
);
es.onmessage = (event) => {
const payload = JSON.parse(event.data);
if (payload.type === "comment.added") {
console.log("New comment:", payload.data);
}
};
Event types on page streams
type | Description |
|---|---|
connected | Sent on connection, includes pageId |
blocks.appended | New blocks added to the page |
action.executed | A page action was executed |
comment.added | New comment posted |
file.uploaded | File successfully committed |
file.deleted | File removed |
Reconnection with Last-Event-ID
The agent SSE stream (/v1/agent/events) supports the standard Last-Event-ID header for gap-filling on reconnect. Missed events since the last received ID are replayed before resuming the live stream.
curl -N https://api.surflet.app/v1/agent/events \
-H "Authorization: Bearer sk_live_..." \
-H "Last-Event-ID: evt_abc123"
PATCH /v1/pages/:id/blocks/:blockId
Incrementally fix the data of an individual content block. Agents can use this endpoint to fix issues after receiving publish warnings, without republishing the entire page.
Request body:
{
"data": { "content": "Corrected content" }
}
You can also simultaneously update type, title, group, and tab.
Response:
{
"pageId": "page_xxx",
"blockId": "b1",
"version": 3,
"block": { "id": "b1", "type": "callout", "data": { "content": "Corrected content" } },
"warnings": []
}
PUT /v1/pages/:id/blocks/reorder
Reorder blocks within a page.
Request body:
{ "block_order": ["block_id_3", "block_id_1", "block_id_2"] }
Blocks not listed are automatically appended to the end.
POST /v1/pages/:id/blocks/insert
Insert a new block at a specific position.
Request body:
{ "position": 2, "block": { "type": "callout", "data": { "content": "Inserted!", "style": "info" } } }
POST /v1/publish/a2ui
Accepts an A2UI v0.9 component list, automatically translates it to Surflet blocks, and publishes a page.
Request body (A2UI envelope):
{
"version": "v0.9",
"title": "User Profile",
"updateComponents": {
"surfaceId": "demo",
"components": [
{ "id": "name", "component": "Text", "text": "Alice Wang", "variant": "h1" },
{ "id": "row", "component": "Row", "children": ["col1", "col2"] },
{ "id": "col1", "component": "Column", "children": ["info"], "weight": 2 },
{ "id": "col2", "component": "Column", "children": ["avatar"], "weight": 1 },
{ "id": "info", "component": "Text", "text": "Engineer @ Acme" },
{ "id": "avatar", "component": "Image", "src": "https://..." }
]
}
}
Also supports passing a component array directly (without the envelope). Row/Column are automatically converted to Surflet layout columns.
POST /v1/publish/ag-ui
Accepts an AG-UI event array, automatically translates it to Surflet blocks, and publishes a page.
Request body:
{
"title": "Agent Analysis",
"events": [
{ "type": "RUN_STARTED", "threadId": "t1", "runId": "r1" },
{ "type": "STEP_STARTED", "stepName": "research" },
{ "type": "TOOL_CALL_START", "toolCallId": "tc1", "toolCallName": "search_db" },
{ "type": "TOOL_CALL_ARGS", "toolCallId": "tc1", "delta": "{\"query\": \"refunds\"}" },
{ "type": "TOOL_CALL_END", "toolCallId": "tc1" },
{ "type": "TOOL_CALL_RESULT", "toolCallId": "tc1", "content": "Found 5 records" },
{ "type": "TEXT_MESSAGE_START", "messageId": "m1", "role": "assistant" },
{ "type": "TEXT_MESSAGE_CONTENT", "messageId": "m1", "delta": "## Summary\n\n..." },
{ "type": "TEXT_MESSAGE_END", "messageId": "m1" },
{ "type": "RUN_FINISHED", "threadId": "t1", "runId": "r1" }
]
}
Event translation rules: text messages → text block, tool calls → code block, results → callout, errors → error callout, steps → stepper, reasoning → collapsible text block.
Authentication
POST /v1/auth/oauth/github
Initiate GitHub OAuth sign-in. Returns a redirect URL.
Response (200):
{ "url": "https://github.com/login/oauth/authorize?..." }
POST /v1/auth/oauth/google
Initiate Google OAuth sign-in. Returns a redirect URL.
Response (200):
{ "url": "https://accounts.google.com/o/oauth2/v2/auth?..." }
GET /v1/auth/me
Get the currently authenticated user or agent.
Response (200):
{
"id": "user_xxx",
"email": "[email protected]",
"tenantId": "tenant_xxx",
"type": "human"
}
POST /v1/auth/logout
Invalidate the current session token.
Response: 204 No Content.
POST /v1/auth/magic-link/send
Send a passwordless magic link to a page viewer.
Request body:
{
"email": "[email protected]",
"pageId": "page_xxx",
"redirectUrl": "https://hai.surf/p/page_xxx"
}
Response (200):
{ "sent": true }
GET /v1/auth/magic-link/verify
Verify a magic link token (called automatically when the user clicks the link).
Query parameters:
| Parameter | Description |
|---|---|
token | The magic link token from the email |
Response: Redirects to redirectUrl with a session token.
POST /v1/auth/agent/register
Create a new Surflet account for an agent without human involvement.
Request body:
{
"name": "my-coding-agent",
"owner_email": "[email protected]"
}
Response (201):
{
"apiKey": "sk_live_...",
"tenantId": "tenant_xxx",
"claimCode": "SURF-7K3M",
"claimUrl": "https://surflet.app/claim",
"message": "Account created. To manage: visit surflet.app/claim and enter code SURF-7K3M"
}
POST /v1/auth/claim
Claim an agent-created account using a claim code.
Request body:
{
"claimCode": "SURF-7K3M"
}
Response (200):
{
"tenantId": "tenant_xxx",
"claimed": true
}
Account
GET /v1/account/api-keys
List all API keys for the current tenant.
Response (200):
{
"keys": [
{
"id": "key_xxx",
"name": "Production Key",
"prefix": "sk_live_abc...",
"createdAt": "2026-03-23T15:00:00Z",
"lastUsedAt": "2026-04-10T09:00:00Z"
}
]
}
POST /v1/account/api-keys
Generate a new API key.
Request body:
{
"name": "Production Key"
}
Response (201):
{
"id": "key_xxx",
"name": "Production Key",
"key": "sk_live_...",
"createdAt": "2026-03-23T15:00:00Z"
}
The full key value is only returned once — save it immediately.
DELETE /v1/account/api-keys/:id
Revoke an API key. All requests using this key will be rejected immediately.
Response: 204 No Content.
Error Responses
All errors use a consistent format:
{
"error": {
"code": "INVALID_REQUEST",
"message": "Field 'title' is required",
"details": {
"field": "title",
"constraint": "required"
}
}
}
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | INVALID_REQUEST | Invalid request parameters |
| 401 | UNAUTHORIZED | API Key is invalid or missing |
| 403 | FORBIDDEN | Insufficient permissions |
| 404 | NOT_FOUND | Resource does not exist |
| 409 | CONFLICT | State conflict (e.g., duplicate action) |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Internal server error |
Rate Limits
| Plan | Limit |
|---|---|
| Free | 60 requests/minute |
| Pro | 600 requests/minute |
| Enterprise | Custom |
When the limit is exceeded, a 429 status code is returned. The Retry-After header indicates how many seconds to wait.