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` partMax file size is `10 MB`. 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 `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`, or folder-sharing endpoints.

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.

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