Platform Guide

API keys and the hooks API

Endpoint reference for triggering workflows, passing files, holding chat sessions, and reading results over signed HTTP calls

Overview

A project API key lets an external system drive a workflow over HTTP without a user session. Requests are signed rather than carrying a token, and every endpoint sits under /api/v1/hooks/{api_key_id}.

This page is the endpoint reference. For creating, rotating and revoking keys, see Role tiers. For how the API trigger fits with the other trigger sources, see Triggers and execution.

The path says hooks for backwards compatibility, but executions started this way are tagged with the api trigger type. This is the public external API.

It is not the same as an inbound webhook, which exists for a third-party sender whose payload you cannot change.

Signing a request

Every call carries two headers:

HeaderValue
X-TimestampA Unix timestamp
X-SignatureHMAC-SHA256(secret, "<timestamp>." + <body>), hex

The key ID travels in the URL. The secret never goes over the wire; it only signs locally.

X-Timestamp must be within 5 minutes of server time. A stale or future timestamp is rejected before the signature is compared at all, which is what stops a captured request being replayed later.

If calls fail authentication while the signature looks correct, check the calling machine's clock first.

Endpoints that take no body sign the empty string. The payload is "<timestamp>." with nothing after the dot.

This applies to file upload, file download, export download, session reads, and reading a human task. Signing the query string or the multipart body instead is the most common reason a correct-looking request returns 404.

Every authentication failure returns 404, never 401 or 403. A wrong signature, an unknown key, a revoked key and a key whose secret cannot be read are indistinguishable, so a key ID cannot be discovered by probing.

If you get a 404 you did not expect, check the signature before you check the path.

Endpoints

Start a workflow

POST /api/v1/hooks/{api_key_id}

Signs the request body.

{
  "use_case_id": "<id or slug>",
  "input": { "document": "<file_id>", "question": "..." }
}

The response carries an execution_id, a poll_token and a poll_url.

BehaviourDetail
VersionRuns the use case's published version. Returns 404 if nothing is published. The version is pinned on the execution record.
ValidationThe input is validated against the Start node's schema. Violations return 422 with a map of field errors.
AsyncThe call returns immediately. Poll for the result.
SyncAdd ?wait=true to block for up to about 60 seconds, a deployment setting. The status code says what happened: 200 if the run finished inside the window, 202 if it did not and you should keep polling.

Poll for status

GET /api/v1/poll/{poll_token}

No authentication. The token is the credential. Rate limited to 60 per minute.

A 200 from ?wait=true means the run finished, not that it succeeded. A run that finished by failing also returns 200, so read the body rather than the status.

When the execution is waiting on a human task, the response includes the pending task IDs. This covers sub-workflows too: if the root run is still going but a child is blocked on a task, polling the root token reports it with the child's task ID. Child executions have no poll token of their own, so you always poll the one root token.

Pass a file in

Files cannot go in the JSON body. Upload first, then reference the ID.

Upload

POST /api/v1/hooks/{api_key_id}/files?extract_text=true
Content-Type: multipart/form-data

Signs the empty body. Returns a file_id.

llm_config_id is required for a scanned PDF or an image, because reading one needs vision. It is ignored for a text document.

Reference it in the trigger input

{ "use_case_id": "...", "input": { "document": "<file_id>" } }

The Start node must declare that field as a file or an image type. Files are scoped to the key's project.

Read a file back out

Two separate endpoints, because an uploaded input and a generated output are different things.

GET /api/v1/hooks/{api_key_id}/files/{file_id}

Reads back a file uploaded over this API. The file must belong to the key's project, which is the same boundary the upload drew.

This exists to close an asymmetry. The ordinary download route needs a user session by construction, so an HMAC caller got a flat 401 from it. A client rendering its own chat history could not show an attachment it had uploaded itself minutes earlier.

GET /api/v1/hooks/{api_key_id}/exports?key=<storage_key>

Downloads a file produced by this key's executions. The storage key comes back in the execution output.

Files endpointExports endpoint
ServesWhat you uploadedWhat a run generated
Identified byfile_idStorage key
ScopeThe key's projectProven to be produced by this key

The exports endpoint is the tighter of the two, because an execution records which key produced its output. An upload records no per-key ownership, so the project is the boundary available to check against.

Answer a human task

GET  /api/v1/hooks/{api_key_id}/human-tasks/{task_id}
POST /api/v1/hooks/{api_key_id}/human-tasks/{task_id}/respond

The GET signs an empty body and returns the task's questions, status and any answers. Use it after polling reports a pending task, to discover the question shape before answering.

The POST signs its body and is idempotent. Add ?wait=true to wait for the workflow to settle after answering.

A key may only read and answer tasks on executions it started. See Human task node.

Hold a chat session

When the target use case's interface type is chat or hybrid, the trigger endpoint runs a conversational path instead of fire-and-forget. The key gets a resumable, memory-bearing thread, while the execution still audits as an API trigger.

{ "use_case_id": "...", "session_id": "<optional>", "input": { "message": "hi" } }
Interface typesession_idResult
formabsentFire and forget
formpresent400, the use case does not support chat
chat or hybridabsentA new session
chat or hybridpresentResumes it, or 404 if it is not this key's

The response echoes both conversation_id and session_id. Store the session ID and send it back to continue the thread.

Threads are owned by the key, not by the person who created it. Two keys never share threads, and a key's threads are a separate namespace from a real user's in-app conversations.

This means a key is a long-lived identity holding conversation history. Treat revoking a key as ending its threads.

Read endpoints, both signing an empty body:

GET /api/v1/hooks/{api_key_id}/sessions?use_case_id=<id or slug>
GET /api/v1/hooks/{api_key_id}/sessions/{session_id}/messages?limit=50

The first returns summaries only, with no message bodies. The second returns a transcript: the newest limit messages, between 1 and 200, oldest-first within that window. Page back through a longer transcript with before_id set to the topmost message ID, until a page comes back empty.

Idempotency

Send an Idempotency-Key header, up to 64 characters, and a retry replays the prior turn's execution rather than starting a second one. The same session_id and execution_id come back.

Use it on every chat-mode trigger. A retry after a dropped connection is otherwise a second turn in the conversation, which is visible to the user and impossible to undo.

Per-key statistics

GET /api/v1/projects/{project_id}/api-keys/{id}/stats?days=7

Session-authenticated, not signed. Returns an execution summary, a time series, the top use cases, and token and cost totals.

days accepts 0 to 90. Pass days=0 for all time.

Good practice

Troubleshooting

Next steps

MagOneAI© 2026 Magure, Inc.

On this page