# Create a new channel (agent) Source: https://docs.placet.io/api-reference/agents/create-a-new-channel-agent /openapi.json post /api/v1/agents # List channels (agents) accessible by this API key Source: https://docs.placet.io/api-reference/agents/list-channels-agents-accessible-by-this-api-key /openapi.json get /api/v1/agents # Remove webhook URL from a channel Source: https://docs.placet.io/api-reference/agents/remove-webhook-url-from-a-channel /openapi.json post /api/v1/agents/deleteWebhook Removes the currently configured webhook so no more events are delivered via HTTP callback. # Set webhook URL for a channel Source: https://docs.placet.io/api-reference/agents/set-webhook-url-for-a-channel /openapi.json post /api/v1/agents/setWebhook Register a webhook URL that will receive events (message:created, review:responded) for the given channel. Replaces any previously configured webhook. # Download file directly by attachment ID Source: https://docs.placet.io/api-reference/files/download-file-directly-by-attachment-id /openapi.json get /api/v1/files/{id}/download # List all files in a channel Source: https://docs.placet.io/api-reference/files/list-all-files-in-a-channel /openapi.json get /api/v1/files # Store a file without creating a message Source: https://docs.placet.io/api-reference/files/store-a-file-without-creating-a-message /openapi.json post /api/v1/files/store # Store text content as a file without creating a message Source: https://docs.placet.io/api-reference/files/store-text-content-as-a-file-without-creating-a-message /openapi.json post /api/v1/files/store-text # Upload a file Source: https://docs.placet.io/api-reference/files/upload-a-file /openapi.json post /api/v1/files/upload # Acknowledge receipt of a message Source: https://docs.placet.io/api-reference/messages/acknowledge-receipt-of-a-message /openapi.json post /api/v1/messages/{id}/ack # Delete (retract) a message Source: https://docs.placet.io/api-reference/messages/delete-retract-a-message /openapi.json delete /api/v1/messages/{id} # Get a single message + review status Source: https://docs.placet.io/api-reference/messages/get-a-single-message-+-review-status /openapi.json get /api/v1/messages/{id} # Get all messages in an iteration chain Source: https://docs.placet.io/api-reference/messages/get-all-messages-in-an-iteration-chain /openapi.json get /api/v1/messages/iterations/{id} # List messages for a channel (chat-as-storage) Source: https://docs.placet.io/api-reference/messages/list-messages-for-a-channel-chat-as-storage /openapi.json get /api/v1/messages # Send a message to a channel (channelId in body) Source: https://docs.placet.io/api-reference/messages/send-a-message-to-a-channel-channelid-in-body /openapi.json post /api/v1/messages # Get plugin details Source: https://docs.placet.io/api-reference/plugins/get-plugin-details /openapi.json get /api/v1/plugins/{name} # List installed plugins Source: https://docs.placet.io/api-reference/plugins/list-installed-plugins /openapi.json get /api/v1/plugins # Get a specific review by message ID Source: https://docs.placet.io/api-reference/reviews/get-a-specific-review-by-message-id /openapi.json get /api/v1/reviews/{id} # List all pending reviews for a channel Source: https://docs.placet.io/api-reference/reviews/list-all-pending-reviews-for-a-channel /openapi.json get /api/v1/reviews/pending # Long-poll for review response (max 30s) Source: https://docs.placet.io/api-reference/reviews/long-poll-for-review-response-max-30s /openapi.json get /api/v1/reviews/{id}/wait # Report agent status (heartbeat) Source: https://docs.placet.io/api-reference/status/report-agent-status-heartbeat /openapi.json post /api/v1/status/ping # WebSocket Source: https://docs.placet.io/api-reference/websocket Real-time events via Socket.IO for agent integrations. Placet exposes a [Socket.IO](https://socket.io/) WebSocket gateway on the `/ws` namespace. Agents can connect to receive real-time events such as new messages, review responses, and status changes. ## Authentication Agents authenticate by passing their API key in the Socket.IO `auth` object: ```typescript theme={null} import { io } from 'socket.io-client'; const socket = io('https://your-placet-instance.com/ws', { auth: { apiKey: 'hp_your-api-key' }, transports: ['websocket'], }); ``` ```python theme={null} import socketio sio = socketio.Client() sio.connect( "https://your-placet-instance.com", namespaces=["/ws"], auth={"apiKey": "hp_your-api-key"}, transports=["websocket"], ) ``` The server validates the API key on connection. If the key is invalid or missing, the connection is rejected before it is established — clients receive a Socket.IO `connect_error` (not a post-`connect` `disconnect`). ## Channel Subscription After connecting, subscribe to a channel to receive its events: ```typescript theme={null} socket.on('connect', () => { socket.emit('subscribe:channel', 'your-agent-id'); }); ``` You can subscribe to multiple channels on the same connection. To unsubscribe: ```typescript theme={null} socket.emit('unsubscribe:channel', 'your-agent-id'); ``` The server verifies that the API key owner also owns the channel. Subscribing to a channel you don't own silently fails. ## Events All events are received on the `/ws` namespace. ### `message:created` Emitted when a new message is posted in a subscribed channel (by a user or another agent). ```json theme={null} { "id": "clxyz111", "channelId": "clxyz456", "senderType": "user", "senderId": "user_abc", "text": "Looks good, ship it!", "status": null, "review": null, "metadata": null, "createdAt": "2025-06-15T14:30:00.000Z", "attachments": [] } ``` ### `review:responded` Emitted when a human completes a review (approval, selection, form, etc.). The full message record is emitted, including attachments. ```json theme={null} { "id": "clxyz111", "channelId": "clxyz456", "senderType": "agent", "senderId": "clxyz456", "text": "Deploy to production?", "status": null, "review": { "type": "approval", "status": "completed", "payload": { "options": [ { "id": "approve", "label": "Approve", "style": "primary" }, { "id": "reject", "label": "Reject", "style": "danger" } ] }, "response": { "selectedOption": "approve", "comment": "Go ahead" }, "completed_at": "2025-06-15T14:32:00.000Z" }, "metadata": null, "createdAt": "2025-06-15T14:30:00.000Z", "attachments": [] } ``` Use the `review.response` field to determine what the human chose: * **Approval**: `response.selectedOption` (`"approve"` or `"reject"`) + optional `response.comment` * **Selection**: `response.selectedIds` (array of selected item IDs) * **Form**: `response.{fieldName}` (key-value pairs for each form field) ### `review:expired` Emitted when a review expires without a response (default: 24 hours). ```json theme={null} { "messageId": "clxyz111" } ``` ### `message:delivery` Emitted when a message's delivery status changes (webhook delivered, agent acknowledged, etc.). ```json theme={null} { "messageId": "clxyz111", "deliveryStatus": "webhook_delivered" } ``` | Status | Meaning | | ------------------- | ------------------------------------- | | `sent` | Message stored, webhook not yet sent | | `webhook_delivered` | Webhook received 2xx response | | `webhook_failed` | Webhook delivery failed | | `agent_received` | Agent explicitly acknowledged receipt | ### `agent:status` Emitted when an agent's ping status changes. ```json theme={null} { "agentId": "clxyz456", "status": "active", "statusMessage": null, "statusSince": "2025-06-15T14:00:00.000Z" } ``` ### `ping` / `pong` Send a `ping` event to check the connection is alive. The server responds with `pong`. ```typescript theme={null} socket.emit('ping'); socket.on('pong', () => console.log('Connection alive')); ``` ## Full Example A complete agent that listens for user messages and review responses: ```typescript theme={null} import { io } from 'socket.io-client'; const BASE_URL = 'https://your-placet-instance.com'; const API_KEY = 'hp_your-api-key'; const CHANNEL = 'your-agent-id'; const socket = io(`${BASE_URL}/ws`, { auth: { apiKey: API_KEY }, transports: ['websocket'], }); socket.on('connect', () => { console.log('Connected'); socket.emit('subscribe:channel', CHANNEL); }); socket.on('message:created', (data) => { if (data.senderType === 'user') { console.log(`User said: ${data.text}`); // Process the message and respond via REST API } }); socket.on('review:responded', (data) => { const response = data.review?.response; if (response?.selectedOption) { console.log(`Review decision: ${response.selectedOption}`); } }); socket.on('review:expired', (data) => { console.log(`Review ${data.messageId} expired`); }); socket.on('disconnect', () => { console.log('Disconnected'); }); ``` WebSocket is best for **interactive agents** that need to react to user input immediately. For background automations, consider [Webhooks](/connections/connection-types#webhooks) or [Long-Polling](/connections/connection-types#long-polling) instead. # Agents Source: https://docs.placet.io/concepts/agents How agents connect to and interact with Placet. ## What is an Agent? An **agent** in Placet represents an external AI system, automation workflow, or no-code tool that sends messages to humans for review. Each agent gets its own chat channel in the inbox. ## Creating an Agent Agents are created from the **inbox sidebar** (click **+ New Agent**), or via the API. Each agent has: * **Name**: displayed in the inbox sidebar * **Description**: optional context about the agent's purpose * **Avatar**: optional visual identifier ## Authentication Agents authenticate via **API Keys**, created in **Settings → API Keys**. API keys are prefixed with `hp_` for easy identification. ```bash theme={null} curl -X POST http://localhost:3001/api/v1/messages \ -H "x-api-key: hp_your-key-here" \ -H "Content-Type: application/json" \ -d '{"text": "Hello from my agent!"}' ``` ## Message Types Agents can send different types of messages: ### Plain text ```json theme={null} { "text": "Deployment completed successfully.", "status": "success" } ``` ### With review (approval buttons) ```json theme={null} { "text": "Deploy v2.1 to production?", "review": { "type": "approval", "payload": { "options": [ { "id": "approve", "label": "Approve", "style": "primary" }, { "id": "reject", "label": "Reject", "style": "danger" } ] } } } ``` ### With plugin rendering ```json theme={null} { "text": "Form submission required", "metadata": { "plugin": "form-submit", "name": "John Doe", "email": "john@example.com" } } ``` ## Review Types Agents can request human input by adding a `review` object to any message. Each review type renders a different interactive UI. ### Approval Presents styled buttons for a yes/no or go/no-go decision. Optionally allows a comment. ```json theme={null} { "type": "approval", "payload": { "options": [ { "id": "approve", "label": "Approve", "style": "primary" }, { "id": "reject", "label": "Reject", "style": "danger" } ], "allowComment": true } } ``` Button styles: `primary`, `danger`, `secondary`, `ghost`. Response: `{ "selectedOption": "approve", "comment": "Looks good" }`. ### Selection Shows radio buttons (single) or checkboxes (multi) for the user to choose from. ```json theme={null} { "type": "selection", "payload": { "mode": "single", "items": [ { "id": "option_a", "label": "Option A", "description": "First choice" }, { "id": "option_b", "label": "Option B", "description": "Second choice" } ] } } ``` Response: `{ "selectedIds": ["option_a"] }`. ### Form Renders a dynamic form with multiple field types. Supports 12 field types: `text`, `number`, `email`, `url`, `textarea`, `select`, `checkbox`, `date`, `time`, `datetime`, `range`, `password`. ```json theme={null} { "type": "form", "payload": { "fields": [ { "name": "environment", "type": "select", "label": "Environment", "required": true, "options": [ { "value": "staging", "label": "Staging" }, { "value": "prod", "label": "Production" } ] }, { "name": "version", "type": "text", "label": "Version", "placeholder": "e.g. 2.1.0" }, { "name": "notify", "type": "checkbox", "label": "Send notification" } ], "submitLabel": "Deploy" } } ``` Response: `{ "environment": "prod", "version": "2.1.0", "notify": true }`. ### Text Input Shows a textarea for free-text responses. Supports markdown preview and character limits. ```json theme={null} { "type": "text-input", "payload": { "placeholder": "Describe the issue...", "markdown": true, "minLength": 10, "maxLength": 2000 } } ``` Response: `{ "text": "The deployment failed because..." }`. ### Freeform Accepts any JSON response. Used primarily with plugins that define their own response UI. ```json theme={null} { "type": "freeform", "payload": {} } ``` Response: any JSON object. ### Review Options All review types support optional fields: | Field | Description | | ------------------ | -------------------------------------------------- | | `expiresInSeconds` | Auto-expire after N seconds (default: 86400 / 24h) | | `expiresAt` | Expire at a specific ISO timestamp (max: 36h) | | `callback` | Legacy inline webhook `{ url, method?, headers? }` | ## Message Status Each message can carry a `status` that controls its visual appearance in the chat: | Status | Use case | | --------- | ----------------------------------- | | `info` | Neutral status update (default) | | `success` | Task completed successfully | | `warning` | Needs attention or pending decision | | `error` | Something went wrong | ## File Attachments Agents can upload files and attach them to messages. Upload a file first, then reference it by ID: ```json theme={null} { "text": "Please review the attached report.", "attachmentIds": ["file-id-from-upload"], "review": { "type": "approval", "payload": { "options": [...] } } } ``` Files are previewed inline in the chat. Supported formats include images, PDFs, video, audio, Office documents, spreadsheets, code files, and more. Users can annotate images directly in the preview. ### Inline HTML Attachments When a message contains a single `text/html` attachment and **Inline HTML rendering** is enabled in chat settings, Placet renders that file inside a sandboxed iframe. This is useful for lightweight interactive demos, status panels, or self-contained UI mockups, but the iframe has important limits: * Use `addEventListener()` instead of inline event attributes like `onclick` * Do not rely on `
` submission * Do not rely on `alert()`, `confirm()`, or `prompt()` * Do not rely on `window.open()` or popup flows * Regular CSS, DOM manipulation, and ` ``` # Creating Plugins Source: https://docs.placet.io/plugins/creating-plugins Step-by-step guide to building a Placet plugin. ## Create a Plugin ```bash theme={null} mkdir packages/plugins/my-plugin ``` ```json theme={null} { "name": "my-plugin", "displayName": "My Plugin", "description": "What this plugin does", "version": "1.0.0", "author": "Your Name", "icon": "./icon.svg", "inputSchema": { "type": "object", "properties": { "title": { "type": "string" }, "url": { "type": "string" } }, "required": ["title"] }, "permissions": { "httpRequests": true, "maxHttpDomains": ["api.example.com"] }, "env": [ { "key": "API_KEY", "label": "API Key", "required": true, "secret": true, "description": "Your API key for the external service." } ] } ``` ```html theme={null}

```
Place an `icon.svg` (or `.png`) in the plugin directory and reference it in `plugin.json`: ```json theme={null} { "icon": "./icon.svg" } ``` ```bash theme={null} make stop && make start ``` The plugin is automatically discovered and available. Go to **Settings → Plugins**, expand your plugin, fill in the environment variables, and click **Save**.
## Manifest Reference ### Top-Level Fields | Field | Required | Description | | ---------------------------- | -------- | ----------------------------------------------------- | | `name` | Yes | Unique plugin identifier (kebab-case) | | `displayName` | Yes | Human-readable name | | `version` | Yes | Semver version | | `description` | No | Short description | | `author` | No | Author name | | `icon` | No | Relative file path (`./icon.svg`) or Lucide icon name | | `inputSchema` | No | JSON Schema for the plugin's input data | | `permissions.httpRequests` | No | Whether the plugin can make HTTP requests | | `permissions.maxHttpDomains` | No | Allowed domains for HTTP requests (`["*"]` = any) | | `env` | No | Array of environment variable definitions | ### Environment Variables Each entry in the `env` array: | Field | Required | Description | | ------------- | -------- | ---------------------------------------- | | `key` | Yes | Variable name (e.g. `API_KEY`) | | `label` | Yes | Human-readable label for the Settings UI | | `required` | No | Whether this variable must be set | | `default` | No | Default value | | `secret` | No | If `true`, rendered as a password field | | `description` | No | Help text shown below the input | Env values are configured per-plugin in **Settings → Plugins** and stored in the database, version-coupled. Plugins access them at runtime via `Placet.env`. ## Validate a Plugin Use the included validation script: ```bash theme={null} npx ts-node scripts/validate-plugin.ts packages/plugins/my-plugin ``` # Plugin Overview Source: https://docs.placet.io/plugins/overview Placet's directory-based plugin system for custom message types. ## What is a Plugin? A **Plugin** defines a **custom message type**. It controls: 1. **What data a message carries** via an input schema (what fields the agent sends) 2. **How that message renders** as HTML/CSS/JS in a sandboxed iframe 3. **What logic runs client-side** such as HTTP requests, data fetching, user interactions 4. **What configuration it needs** like environment variables (API keys, URLs, etc.) A Plugin does **NOT** control: * Whether a message requires a review/response (that's the agent's decision per message) * The approve/reject buttons (that's the review system, orthogonal to plugins) * Authentication or routing (that's the platform) ### Plugin vs Review These are **two independent axes** on a message: ``` Message ├── metadata.plugin: "form-submit" ← HOW the message renders (Plugin) ├── review: { ... } ← WHETHER user input is needed (Review) │ ├── type: "approval" ← WHAT kind of input │ └── payload: { options: [...] } └── metadata: { name: "John", ... } ← Plugin-specific data ``` ## Plugin Structure Each plugin lives in `packages/plugins//` and consists of: * [**plugin.json**](/plugins/creating-plugins#manifest-reference): Manifest with metadata, input schema, env variables, and permissions * [**render.html**](/plugins/creating-plugins#create-renderhtml): HTML + CSS + JS rendered inside a sandboxed iframe * **icon.svg/.png** (optional): Icon shown in the Settings UI Plugins are discovered automatically on backend startup. No build step required, just add the directory and restart. ## Built-in Plugins Placet ships with two example plugins you can use as reference: | Plugin | Description | Source | | ----------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | **Form Submit** | Renders a form from structured data and submits it to a configurable webhook URL | [`packages/plugins/form-submit/`](https://github.com/placet-io/placet/tree/main/packages/plugins/form-submit) | | **Kroki Diagram** | Renders diagrams (Mermaid, PlantUML, D2, etc.) via a Kroki server | [`packages/plugins/kroki-diagram/`](https://github.com/placet-io/placet/tree/main/packages/plugins/kroki-diagram) | ## Data Flow ```mermaid theme={null} sequenceDiagram participant Agent as Agent participant API as Placet API participant UI as Web App participant Plugin as Plugin iframe Agent->>API: POST /api/v1/messages
metadata: { plugin: "form-submit", ... } API->>UI: WebSocket: message:created UI->>Plugin: Load render.html in sandboxed iframe Plugin->>Plugin: Placet.data, Placet.env available Plugin->>API: Placet.fetch() (server-side proxied) ``` ## Security Model | Concern | Solution | | -------------------- | ---------------------------------------------------------------------------- | | **DOM access** | `sandbox="allow-scripts"` only, no `allow-same-origin`, no parent DOM access | | **Cookies/Storage** | Sandboxed iframe has no access to parent cookies or localStorage | | **HTTP requests** | Server-side proxied; domain allowlist enforced via `maxHttpDomains` | | **Script injection** | Plugin HTML is static per-plugin, loaded from disk | | **Env values** | Stored in DB, injected at render time; secrets not exposed in manifest | ## Available in Plugins Inside the iframe, plugins access the [Bridge API](/plugins/bridge-api) via the `Placet` global: | Property | Type | Description | | -------------------- | ------------------------- | -------------------------------------------------------------- | | `Placet.data` | `Record` | Plugin input data from the message metadata | | `Placet.env` | `Record` | Environment variables configured in Settings | | `Placet.attachments` | `AttachmentInfo[]` | Array of attached files (`{ id, filename, mimeType, size }`) | | `Placet.message` | `MessageContext` | Message context (`id`, `channelId`, `senderType`, `createdAt`) | | `Placet.theme` | `'light' \| 'dark'` | Current theme | | `Placet.review` | `ReviewContext \| null` | Review context (`{ type, status, payload }`) or `null` | | `Placet.isPreview` | `boolean` | `true` when rendered in the full-screen preview modal | Methods: `Placet.fetch()`, `Placet.getFile()`, `Placet.getFileUrl()`, `Placet.toast()`, `Placet.respond()`, `Placet.resize()`, `Placet.emit()`, `Placet.on()`. See the full [Bridge API reference](/plugins/bridge-api) for details. # Quickstart Source: https://docs.placet.io/quickstart Get Placet running locally in under 5 minutes. ## Prerequisites * **Git** * **Docker & Docker Compose** ## Setup `git clone https://github.com/placet-io/placet.git && cd placet` `cp .env.example .env` Edit `.env` to set your desired admin email, password, and other settings. `make setup` This installs dependencies, builds packages, starts all Docker services (Postgres, MinIO, Backend, Frontend), runs migrations, and creates the initial user. ## Access the services Once setup completes, the following services are available: | Service | URL | | ----------------- | -------------------------------- | | **Frontend** | `http://localhost:3000` | | **Backend API** | `http://localhost:3001` | | **Swagger Docs** | `http://localhost:3001/api/docs` | | **MinIO Console** | `http://localhost:9001` | **Default login:** `admin@placet.local` / `changeme` (configurable in `.env`) ## Send your first agent message Go to **Settings → API Keys** and create a new key. Click **+ New Agent** in the inbox sidebar to create a new agent. ```bash theme={null} curl -X POST http://localhost:3001/api/v1/messages \ -H "x-api-key: hp_your-key-here" \ -H "Content-Type: application/json" \ -d '{"channelId": "your-agent-id", "text": "Hello from my agent!", "status": "success"}' ``` Open the agent's chat in the frontend and your message appears in real-time. ## Request human approval ```bash theme={null} curl -X POST http://localhost:3001/api/v1/messages \ -H "x-api-key: hp_your-key-here" \ -H "Content-Type: application/json" \ -d '{ "channelId": "your-agent-id", "text": "Deploy v2.1 to production?", "review": { "type": "approval", "payload": { "options": [ { "id": "approve", "label": "Approve", "style": "primary" }, { "id": "reject", "label": "Reject", "style": "danger" } ] } } }' ``` The message appears with approval buttons. When the user clicks one, you receive the response via webhook or by polling the API. ## Next steps Understand how agents and humans interact in Placet Build custom message types with the plugin system Full REST API documentation Deploy Placet on your own infrastructure # Self-Hosting Source: https://docs.placet.io/self-hosting Deploy Placet on your own infrastructure with Docker Compose. ## Requirements * Docker & Docker Compose v2 * At least 2 GB RAM ## Docker Compose Deployment Placet ships with a production-ready `docker-compose.yml` that includes all required services. `git clone https://github.com/placet-io/placet.git && cd placet && cp .env.example .env` Edit `.env` with your production settings: - Set strong passwords for `POSTGRES_PASSWORD`, `MINIO_ROOT_PASSWORD` - Set `JWT_SECRET` to a secure random string - Configure `INITIAL_USER_EMAIL` and `INITIAL_USER_PASSWORD` - Set `NEXT_PUBLIC_WS_URL` and `NEXT_PUBLIC_APP_URL` to your domain `docker compose up -d` `docker compose exec backend npx prisma migrate deploy` ## Services | Service | Port | Description | | ------------ | ---- | ---------------------------- | | **Frontend** | 3000 | Next.js web application | | **Backend** | 3001 | REST API server | | **Postgres** | 5432 | PostgreSQL database | | **MinIO** | 9000 | S3-compatible object storage | ## Reverse Proxy A Traefik configuration is included in `docker-compose.traefik.yml` for production deployments with TLS/SSL. ``` docker compose -f docker-compose.yml -f docker-compose.traefik.yml up -d ``` ## Environment Variables See `.env.example` in the repository for a complete list of all available environment variables with descriptions. ### Core Settings | Variable | Description | Default | | ----------------------- | ------------------------------ | ----------------------- | | `DATABASE_URL` | PostgreSQL connection string | — | | `JWT_SECRET` | Secret for JWT token signing | — | | `INITIAL_USER_EMAIL` | Initial admin user email | `admin@placet.local` | | `INITIAL_USER_PASSWORD` | Initial admin user password | `changeme` | | `NEXT_PUBLIC_WS_URL` | WebSocket URL for the frontend | `http://localhost:3001` | | `NEXT_PUBLIC_APP_URL` | Public URL of the frontend | `http://localhost:3000` | ### File Storage (MinIO/S3) | Variable | Description | Default | | ------------------ | ------------- | ----------- | | `MINIO_ENDPOINT` | MinIO/S3 host | `localhost` | | `MINIO_PORT` | MinIO/S3 port | `9000` | | `MINIO_ACCESS_KEY` | Access key | — | | `MINIO_SECRET_KEY` | Secret key | — | | `MINIO_BUCKET` | Bucket name | `placet` |