Technical docs

Private API

Overview

Support reference for the organization-scoped private API under `/api/private`.

Auth

  • Current auth is the organization private API key.
  • The middleware accepts either `Authorization: Bearer <token>` or the raw key in the `api-key` header.
  • Example:

```bash curl -H "Authorization: Bearer <TOKEN>" "https://api.alloy.cx/api/private/storage/" ```

Organization status and API access

When an organization's `status` is `inactive`, the private API blocks all mutating methods:

  • `POST`, `PUT`, `PATCH`, and `DELETE` return `403` with `{ error: "Organization is inactive", code: "ORGANIZATION_INACTIVE" }`.
  • Side-effecting `GET` endpoints (e.g. `GET /api/private/stargate/?system=...`) are also blocked.
  • Read-only `GET` endpoints (storage listing, metadata, downloads) continue to work.

Current route groups

  • `/api/private/storage`
  • `/api/private/trigger`
  • `/api/private/stargate`
  • `/api/private/employee`

Storage routes

Base URL: `{api_url}/api/private/storage`

These routes expose full organization storage access.

Call these routes from a trusted external service or narrow gateway. Do not inject the private API key into a structured workflow's `Run Code` or tool parameters without user's approval.

MethodPathQuery / BodyNotes
`GET``/``?path=` optionalLists direct children of a folder. `path` defaults to `/`. Returns `404` with `{ success: false, error: "Storage path not found" }` when the folder does not exist or the path resolves to a file. A valid empty folder returns an empty list.
`POST``/upload``multipart/form-data` with optional `path` and required `file` partThe per-file size ceiling comes from the backend `STORAGE_MAX_UPLOAD_SIZE_MB` setting (default `10 MB`), shared with the organization upload route. The stored filename comes from the uploaded file's original filename. Omitting `path` uploads to storage root. Existing files are overwritten.
`GET``/meta``?path=` requiredFile-only metadata lookup. Returns `path`, `name`, `size`, `created_at`, `modified_at`, `hash`, and `mime_type`. Returns `400` when the path is a folder and `404` when the file is missing.
`DELETE``/file``?path=` requiredDeletes a file or folder. Returns `400` for root deletion attempts.
`GET``/download``?path=` requiredFile-only download. Returns `400` when the path is a folder.
`POST``/folder`JSON `{ name, path? }`Creates a folder and returns its canonical relative path.
`POST``/rename`JSON `{ path, new_name }`Renames a file or folder in place.
`POST``/move`JSON `{ source_path, dest_path }`Moves a file or folder into the destination directory. `dest_path` is a directory path, not a full target filename.

Storage response notes

  • Successful storage mutations return `success: true` and usually a `data.path` field with the canonical relative path.
  • Upload validation failures return `400`.
  • Uploads above the configured `STORAGE_MAX_UPLOAD_SIZE_MB` limit (default `10 MB`) return `413`.
  • Upload has no replace/skip flag. If the target file already exists, the server truncates and rewrites it, returns `200`, and emits `storage_file.updated`.
  • Upload has no conflict status for existing files, no ETag / `If-Match` precondition, and no optimistic concurrency control. Clients should treat duplicate uploads as last-write-wins.
  • `GET /storage/meta` prefers a cached document hash when one already exists in `organization_documents.file_hash`; otherwise it computes the hash from the current file contents on demand.
  • `POST /storage/rename` and `POST /storage/move` return `409` on target-path conflicts.
  • The current private storage API does not expose `copy`, `stats`, folder-sharing, or managed database routes. Authenticated organization database routes are instead under `/api/organizations/{organization_id}/storage/databases`; see `databases.md`.
  • Private storage upload follows the same extension and content-validation registry as the organization Storage upload route. The backend checks detected content where required and assigns the canonical MIME for the accepted extension instead of trusting the supplied MIME. SQLite database extensions (`.sqlite`, `.db3`, `.db`, `.sqlite3`) require detected SQLite content; uploading a database file does not automatically create a managed database alias. See `storage.md` for the supported upload formats.

Trigger route

Base URL: `{api_url}/api/private/trigger`

MethodPathBodyNotes
`POST``/{workflow_id}`Arbitrary JSON payloadStarts the workflow from its configured starting step and returns the created run ID plus the immediate queue result.

Public trigger URL format

  • The public trigger route accepts `{api_url}/api/public/trigger/{publicApiKey}/{workflowId}`.
  • The backend URL-encodes both dynamic path segments when generating those URLs.
  • Newly generated organization `public_api_key` values are 32-character lowercase hex strings, which are safe to use as URL path segments.

Inbound trigger security

  • The private trigger authenticates the caller with the organization private API key.
  • The public trigger uses the key in its URL to identify the organization. Do not treat the URL key as proof that a request came from an expected third-party provider.
  • Neither trigger provides generic verification for vendor signatures, request timestamps, nonces, or replayed events.
  • Public and private trigger routes accept the parsed request body without enforcing the workflow's declared payload schema.
  • When a provider signs its webhooks, verify the raw request body, signature, and timestamp in an ingress service or gateway. Forward a normalized payload to Alloy only after verification succeeds.
  • Include the provider's stable event ID in the forwarded payload. Use it as a deduplication or idempotency key before causing side effects.
  • Reject events outside the provider's allowed timestamp window. Keep signing secrets in the gateway or approved secret configuration, not in workflow payloads or launcher instructions.
  • Immediate public and private trigger calls process the workflow synchronously before responding. A debounced public trigger is the exception because it records the event for later execution.
  • For webhook providers with short acknowledgement deadlines, validate and durably queue the event in a gateway. Return the provider's expected success response before a worker invokes Alloy.

Trigger response notes

  • Returns `404` when the workflow does not exist in the authenticated organization.
  • Returns `404` when the workflow exists but has no `flow` or no `starting_step_id`.
  • On success returns:
  • `success: true`
  • `data.runId`
  • `data.result`

Employee message routes

Base URL: `{api_url}/api/private/employee`

These routes let a server-side integration connect a custom chat system to an Alloy AI teammate. For the full external developer guide, see `references/custom-channel-integration.md`.

MethodPathQuery / BodyNotes
`POST``/{employee_id}/messages`JSON body with required `message` and `channel`; optional all-or-nothing `conversation` and `contact`; optional `attachments[]` with base64 file data; optional `is_async`Sends one user message to an AI teammate. Synchronous execution is the default; `is_async: true` queues the run for polling. With `conversation` and `contact`, the chat is saved and can continue later. Without them, the request is one-off and no chat history is saved.
`GET``/{employee_id}/messages``conversation_id`, `channel`, optional `page`, optional `limit` up to `50`Lists saved messages for a custom chat conversation. Use the same `channel` and `conversation_id` values that were used when sending messages.
`PUT``/{employee_id}/messages/{message_id}/feedback`JSON with required `channel`, `contact_id`, and nullable `reaction`; optional `feedback_comment`Sets or clears feedback on a saved AI message. `reaction` is `like`, `dislike`, or `null`; `feedback_comment` is limited to 500 characters.

Employee message request notes

  • The `contact` object accepts optional `id`, `first_name`, `last_name`, `email`, and `phone` fields. Supplied fields update the contact channel profile when the saved chat is created or continued.
  • `conversation` and `contact` remain all-or-nothing: sending only one returns `400`.
  • `attachments` defaults to `[]` when omitted. Only image attachments are forwarded into workflow input data; all attachments stay on the saved message.
  • `is_async` is optional and defaults to `false`. Set it to `true` when the integration should receive a run ID immediately and poll the private workflow-run endpoint instead of waiting for completion.
  • Attachment items require `size` to be an integer of at least `1`. The server validates decoded base64 content size against the configured chat attachment limits (`CHAT_MAX_ATTACHMENTS`, `CHAT_MAX_ATTACHMENT_SIZE_MB`, `CHAT_MAX_TOTAL_ATTACHMENT_SIZE_MB`; defaults `10`, `10`, `30`).
  • When attachments exceed those limits, `POST` returns `422` with error code `ATTACHMENT_LIMIT_EXCEEDED`, a `reason` of `MAX_ATTACHMENT_COUNT`, `MAX_ATTACHMENT_SIZE`, or `MAX_TOTAL_ATTACHMENT_SIZE`, the offending `files`, and the `limits` object.

Employee message response notes

  • `POST /employee/{employee_id}/messages` returns `success: true`, `data.runId`, `data.message`, `data.messageId`, `data.assistantMessageId`, and `data.attachments`.
  • With synchronous execution, `data.message` contains the immediate workflow result and `data.attachments` contains generated workflow output attachments. Each item is `{ name, mime_type, size, data }` with base64-encoded file content, including encrypted attachments.
  • With `is_async: true`, `data.message` is `null` and `data.attachments` is `[]`; use `data.runId` with `GET /api/private/workflow/run/{run_id}` to retrieve status and the completed output.
  • `data.messageId` is `null` for one-off requests where `conversation` and `contact` are omitted. A saved asynchronous chat still returns the persisted incoming message ID.
  • `data.assistantMessageId` identifies the saved AI reply for a synchronous saved chat. It is `null` for asynchronous and one-off requests.
  • `POST` returns `404` when the AI teammate does not exist in the authenticated organization.
  • `POST` returns `422` when the AI teammate is not ready to process messages or when attachments exceed the configured chat attachment limits.
  • `GET` returns message objects plus pagination metadata.
  • `GET` returns `404` when no saved chat exists for the supplied `channel` and `conversation_id`.
  • The feedback route verifies the message inside the requested AI teammate, channel conversation, contact, and participant scope. A missing or out-of-scope message returns `404` with `{ "error": "Message not found" }`.

Workflow-run polling route

Base URL: `{api_url}/api/private/workflow`

MethodPathNotes
`GET``/run/{run_id}`Retrieves a workflow run in the authenticated organization. Returns `data.runId`, current `data.status`, saved incoming `data.messageId`, and saved AI reply `data.assistantMessageId`. While unfinished, `data.result` is `null`; when finished, `data.result` contains the run's last output and `data.attachments` contains generated output files.
  • A missing or cross-organization run returns `404` with `{ "error": "Workflow run not found" }`.
  • Finished output attachments use the same `{ name, mime_type, size, data }` base64 shape as synchronous employee-message responses.

Stargate routes

Base URL: `{api_url}/api/private/stargate`

Stargate lets a gateway process poll Alloy for work routed to a named internal system, execute it against that internal system, and report the result back to Alloy. For the architecture and setup model, see `stargate.md`.

MethodPathQuery / BodyNotes
`GET``/``?system=` requiredLong-polls for up to `60` seconds for pending tasks for the named internal system. Returns early when tasks are available; otherwise returns `success: true, data: []`.
`POST``/{task_id}`Any request bodyRecords the posted body as the task response and flips the task from `requested` to `responded`. The current public Stargate gateway posts a JSON envelope with `ok`, `status`, `body`, and optional `error`, but the Alloy route itself does not enforce a response schema.

The gateway executes polled tasks with JSON bodies by default. For non-`GET`/`HEAD` tasks whose `body` is exactly `{ "body_b64": "<base64>" }`, it validates and decodes the string and forwards the bytes to the internal system; supply the appropriate `Content-Type` in task headers. Other body shapes remain JSON, and invalid base64 fails without calling the internal system. See `stargate.md` for header precedence and binary-body behavior.

Stargate response notes

  • The gateway uses the same `api-key` authentication as the rest of the private API.
  • `system` is a routing key. A gateway should only receive tasks for the internal system name it passes in the poll request.
  • Returned tasks include `stargate_task_id`, `organization_id`, `system`, `method`, `uri`, `body`, `response`, `status`, `created_at`, and `updated_at`.
  • Task statuses currently move through `created`, `requested`, `responded`, `completed`, or `failed`.
  • The `GET` route checks for new work every `5` seconds during the long-poll window.
  • The `POST` route currently accepts JSON, plain text, raw bytes, or empty bodies and stores the payload as text.
  • `POST /{task_id}` returns `404` when the task is missing.
  • `POST /{task_id}` returns `400` when the task is not currently in `requested` status.

Start building your AI team