API quickstart
Use Plaine’s agent API when you own the agent runtime and want to exchange messages with Plaine over HTTP.
Start in the Plaine dashboard:
- Open Agents and create an agent.
- Choose a delivery mode:
- Pull for MCP clients, local runners, and agents that cannot receive public webhooks.
- Push for webhook servers that can receive Plaine POSTs.
- Create an API key from the agent’s settings. Open API keys. Use a name of at most 32 characters, and copy the key immediately; Plaine only shows it once.
- Register agent encryption keys before your runtime participates in private E2EE conversations.
Authenticate
Send the agent API key as either x-api-key or a Bearer token:
x-api-key: plane_xxx
Authorization: Bearer plane_xxx
Examples below use x-api-key.
export PLAINE_API_BASE=https://plaine.chat
export PLAINE_API_KEY=plane_xxx
Pull mode
Pull mode is the simplest shape for local agents. The agent creates or finds a conversation, sends messages into it, and polls for user replies.
Create or find a conversation with you, the agent owner, by label:
curl -s "$PLAINE_API_BASE/api/agent/conversations" \
-H "x-api-key: $PLAINE_API_KEY" \
-H "content-type: application/json" \
-d '{"label":"local-dev"}'
Response:
{
"conversationId": "conv_...",
"label": "local-dev",
"created": true
}
Send an agent message:
curl -s "$PLAINE_API_BASE/api/agent/conversations/conv_.../messages" \
-H "x-api-key: $PLAINE_API_KEY" \
-H "content-type: application/json" \
-d '{"parts":[{"type":"text","text":"I need a decision before I continue."}]}'
Response:
{ "id": "msg_..." }
For retryable sends, supply a stable clientMessageId and resend the same payload after a timeout. Plaine returns the accepted message without overwriting it. Reusing that ID with different content returns HTTP 409 with code message_conflict; treat this as a conflict, not a retryable outage.
For plaintext requests, Plaine preserves the first accepted server timestamp on retries. Encrypted requests must reuse the identical envelope, including its authenticated timestamp.
Ask for tool approval by sending a tool part with state: "approval-requested":
curl -s "$PLAINE_API_BASE/api/agent/conversations/conv_.../messages" \
-H "x-api-key: $PLAINE_API_KEY" \
-H "content-type: application/json" \
-d '{
"parts": [
{
"type": "tool-shell",
"toolCallId": "call_deploy_1",
"state": "approval-requested",
"input": { "command": "pnpm deploy:staging" }
}
]
}'
The peer’s decision returns as a normal user message part:
{
"type": "data-tool-approval",
"id": "approval_...",
"data": {
"toolCallId": "call_deploy_1",
"approved": true,
"reason": "optional short text"
}
}
Resume the paused work only after you receive a matching data-tool-approval part with the same toolCallId.
Poll the inbox for user messages:
curl -s "$PLAINE_API_BASE/api/agent/conversations/conv_.../inbox?wait=25" \
-H "x-api-key: $PLAINE_API_KEY"
Response:
{
"messages": [
{
"id": "msg_...",
"conversationId": "conv_...",
"senderType": "user",
"timestamp": "2026-05-13T12:00:00.000Z",
"parts": [{ "type": "text", "text": "Use the safer migration." }]
}
],
"nextCursor": "msg_..."
}
Pass since=<message_id> on the next inbox request to receive only newer user messages. wait enables long-polling; Plaine caps a single wait at 30 seconds.
Encrypted attachments
Attachments are an unreleased private-E2EE-only capability, available only on isolated staging where the private E2EE path is enabled. For coding agents, prefer Coding Agents MCP, which performs this flow locally. Implement the HTTP contract below only when you own the runtime and its E2EE keys.
Send an attachment
For every file, allocate an attachment ID, encrypt the file and its manifest locally, upload the ciphertext, then send one plaine.message.e2ee.v3 envelope. Its authenticated attachmentRefs must name the attachment ID, and its encrypted plaintext must include the matching manifest.
Do not send plaintext parts, attachments, or metadata alongside a private E2EE envelope.
Allocate the ID with your agent API key and the active runtime device that holds the conversation key:
POST /api/agent/conversations/:conversationId/attachments
x-api-key: plane_xxx
content-type: application/json
{
"messageId": "msg_...",
"senderDeviceId": "adev_...",
"keyEpoch": 1
}
Plaine returns an attachmentId, uploadUrl, and expiry time. Encrypt in 1 MiB chunks with the attachment protocol, then upload only the resulting ciphertext:
PUT /api/agent/conversations/:conversationId/attachments/:attachmentId/upload
x-api-key: plane_xxx
content-type: application/octet-stream
content-length: <ciphertext byte count>
x-plaine-ciphertext-sha256: <base64url SHA-256 of ciphertext>
<ciphertext bytes>
Finally, send the v3 envelope through POST /api/agent/conversations/:conversationId/messages. Use the same message ID for clientMessageId and the envelope’s messageId. Put the attachment ID in both the envelope’s authenticated attachmentRefs and the manifest encrypted in its plaintext.
Receive an attachment
Decrypt the message envelope first. Its encrypted plaintext contains the attachment manifest, including the attachment ID and the values required to derive the attachment key. Download ciphertext with the recipient runtime device, then decrypt and authenticate every chunk locally:
GET /api/agent/attachments/:attachmentId/download
x-api-key: plane_xxx
x-plaine-device-id: adev_...
Only download a manifest received in the same private conversation and authenticated by its message envelope. A local runtime should require an explicit absolute destination path and create it without overwrite; if transfer or authentication fails, remove any partial file.
Limits and privacy
- A message may contain at most 10 files.
- The combined plaintext size is at most 100 MB per message. A single file cannot exceed that limit.
- Filename, MIME type, plaintext size, and the attachment manifest are encrypted inside the message envelope. Plaine cannot read them.
- Plaine can see routing and lifecycle metadata, ciphertext size and checksum, and ciphertext needed to store and deliver the attachment.
- An upload that is never bound to a message expires after seven days. Cancel an unbound upload when you know the message will not be sent.
Audio files use this same attachment contract. In a private E2EE conversation on isolated staging, browser peers can record a voice note for up to five minutes when the browser negotiates audio/webm or audio/mp4. They can preview or remove it before sending. Its filename, exact MIME type, duration, and plaintext byte size are encrypted in the attachment manifest.
Microphone permission is handled locally. If a peer declines it, they can still attach an existing audio file. Plaine does not transcribe, generate waveforms for, or transcode voice notes; it receives and delivers ciphertext only.
Failures to handle
- Allocate before encrypting: the allocated ID is part of the manifest and authenticated ciphertext.
- Reuse the same ciphertext length and SHA-256 digest when retrying an upload. A completed upload with different ciphertext is rejected.
- If sending the message fails after upload, cancel the unbound attachment with
DELETE /api/agent/attachments/:attachmentId. - A bound attachment is immutable and cannot be cancelled or uploaded again.
Push mode
Use push mode when your agent has a public webhook server. Plaine POSTs new user messages to your webhook, and your server replies through the same message endpoint. Use the conversationId from the webhook payload:
curl -s "$PLAINE_API_BASE/api/agent/conversations/conv_.../messages" \
-H "Authorization: Bearer $PLAINE_API_KEY" \
-H "content-type: application/json" \
-d '{"parts":[{"type":"text","text":"Thanks, I will continue."}]}'
For push mode, always verify Plaine’s webhook signature before trusting the request. See Webhook signing.
Webhook delivery is at-least-once. Deduplicate using the envelope’s stable deliveryId, including concurrent requests. Durably accept work before returning a successful response, and make downstream effects idempotent; signature verification alone does not prevent duplicate processing.
Approval responses use the same message.new webhook envelope as text replies. Match message.parts[].data.toolCallId to the pending tool call before resuming the tool.
Related setup
- Coding Agents MCP — use Plaine from Claude Code, Codex, Cursor, or generic MCP clients without writing HTTP glue.
- Agent encryption keys — register public key material and an active runtime device for future encrypted peer readiness.
- Webhook signing — verify Plaine webhook deliveries in push mode.
- Owner metadata — attach your own user mapping to invites and read it back on webhook deliveries.