API
Ingest and decide HTTP endpoints.
Authenticate ingest with a project API key: Authorization: Bearer hitly_...
The key must belong to HITLY_PROJECT_ID. Send Idempotency-Key when the origin may retry the suspend POST. A matching open item (pending or failed_resume) is replayed. After reject, cancel, decide, or expiry, the same key creates a new inbox item.
Create approval
POST /api/v1/approvals
{
"plugin": "mastra",
"projectId": "prj_...",
"kind": "workflow",
"workflowId": "refund-workflow",
"runId": "run_...",
"stepId": "hitly-approval",
"action": { "name": "send-refund", "args": { "orderId": "1842" } },
"suspendPayload": { "reason": "Human approval required." }
}mastraBaseUrl is optional when the project already stores the origin base URL.
HTTP origins send plugin: "http", resumeUrl, and optional metadata. Hitly echoes metadata on the resume POST:
{
"plugin": "http",
"projectId": "prj_...",
"runId": "execution-or-page-id",
"actionName": "http-callback",
"metadata": { "pageId": "…" },
"resumeUrl": "https://your-callback.example/hitly-resume"
}On accept or reject, Hitly POSTs that metadata unchanged to resumeUrl:
{
"decision": "accept",
"id": "apr_...",
"metadata": { "pageId": "…" }
}Decide
POST /api/v1/approvals/:id/decide (session cookie)
{ "decision": "accept" }or { "decision": "edit", "editedArgs": { "amount": 2000 } }.
The JSON response includes resumeResponse: the payload sent to the origin (resumeData) plus the origin HTTP status and body. For Mastra workflows that body is the completed run result.
Retry resume
POST /api/v1/approvals/:id/retry-resume when status is failed_resume.
Force cancel
POST /api/v1/approvals/:id/cancel (session) when status is pending or failed_resume. Closes the item as cancelled without calling the origin. Use when the origin run is no longer suspended.
Inbox
GET /api/v1/inbox (session cookie or Authorization: Bearer <session token>)
Query: scope=all|open|closed, q, projectId. open is pending + failed resume. closed is decided + expired + cancelled.
GET /api/v1/inbox/summary returns { pending, failedResume, decidedToday, projectCount }.
GET /api/v1/approvals/:id returns envelope, origin, originFields, decisions, and canAct (session cookie or reviewer Bearer token).
A project API key on the same route returns a slim origin-poll payload: { id, status, decision?, response? }. Hermes uses this while present() waits and while the gateway kanban poller applies comment + unblock.
Workspaces and devices
GET /api/v1/workspaces lists memberships. POST /api/v1/workspaces/current with { "workspaceId" } selects one. Mobile also sends x-hitly-workspace-id.
GET /api/v1/workspace returns the current workspace (name, timezone, default SLA, canManage). PATCH /api/v1/workspace with { "name", "timezone", "sla" } updates it (workspace admins).
GET /api/v1/projects lists visible projects. GET /api/v1/projects/:id is the summary. GET/PATCH /api/v1/projects/:id/config is project config for admins (name, SLA, assignee, origin URL; token is write-only). Ingest API keys stay on the web app.
POST /api/v1/devices registers an Expo push token ({ "token", "platform": "ios"|"android" }). DELETE /api/v1/devices unregisters { "token" }. Project API keys are ingest-only — never use them from the reviewer app.
GET /api/v1/health is public: { "ok": true, "product": "hitly", "edition": "oss"|"cloud" }.
POST /api/v1/housekeeping runs due jobs (Authorization: Bearer $HITLY_CRON_SECRET, or unauthenticated in development when the secret is unset). Body is optional: { "job": "expire-overdue-approvals", "force": true }.
See Mobile for the Expo app, deep links, and push payload.