Adapter Contract Reference
Every App adapter is a thin HTTP shell that handles channel logic only — receiving webhooks, normalizing inbound payloads, delivering outbound messages, and platform-specific formatting. Agent-callable platform operations (search, create, send, etc.) live in Python tools attached to the App's Skill, not in the adapter. OAuth lifecycle is owned by ORQO (initial code exchange and refresh) — the adapter never sees an OAuth code or refresh request.
ORQO calls the adapter's endpoints to verify webhook signatures, parse inbound events, deliver outbound messages, and (optionally) sync document sources. Adapters are stateless — credentials are passed per-request.
Endpoints
GET /manifest.json
Serve the adapter's manifest. ORQO fetches this when a developer registers the adapter in the Developer Portal and when clicking Refresh on an existing listing.
Response:
{
"schema_version": "1.0",
"name": "My Service",
"description": "Connect ORQO agents to My Service.",
"icon": "puzzle-piece",
"category": "productivity",
"capabilities": [],
"credentials": [
{ "key": "MY_SERVICE_TOKEN", "type": "api_key", "label": "API Key", "maps_to": "token" }
],
"skill": { "name": "My Service", "knowledge": "..." },
"setup_instructions": ["..."],
"author": { "name": "Your Company", "url": "https://example.com" }
}
See Building an App for the complete manifest schema.
The manifest declares the adapter's channel capabilities and credential requirements. Agent-callable tools are not part of the adapter — they live as Python tools on the App's Skill.
GET /health
Health check. ORQO polls this to confirm the adapter is running and to confirm the right code is live.
Response:
{ "status": "ready", "version": "abc123", "adapter": "my_service" }
| Field | Required | Notes |
|---|---|---|
status | yes | Must be exactly "ready" while the adapter is serving traffic. Anything else is treated as un-ready. |
version | yes | The git sha (or build identifier) of the running container. Lets ORQO confirm the right code is live after a rollout. Use "unknown" if you can't inject one. |
adapter | yes | Your adapter slug, matching the slug field in the manifest and the URL path your adapter is mounted under. |
POST /verify
Test that credentials are valid by making a lightweight call to the external API.
Request body:
{
"credentials": {
"access_token": "ya29.a0AfH6SM...",
"client_id": "...",
"client_secret": "..."
}
}
The credentials object contains all fields from the App's linked credential record, mapped through the maps_to field declared in the manifest's credentials array.
Success response:
{ "ok": true, "user": "alice@example.com" }
Failure response:
{ "ok": false, "error": "Invalid or expired token" }
The adapter does not expose a /mcp endpoint. Platform operations agents can call (search, create, send, etc.) live as Python tools on the App's Skill and run inside ORQO's engine. They share the App's credential row but are authored and executed independently of the adapter. See Building a Skill.
POST /deliver (Channel Apps Only)
Send a message outbound to the external platform. Only required for Apps with send capability.
Request body:
{
"credentials": { "token": "xoxb-..." },
"channel_address": "C01234567",
"message": {
"text": "Here is the analysis you requested.",
"thread_id": "1234567890.123456"
}
}
POST /verify-webhook (Channel Apps Only)
Verify the signature of an inbound webhook payload. Only required for Apps with receive capability.
POST /parse-inbound (Channel Apps Only)
Normalize an inbound webhook payload into ORQO's standard format. Returns routing flags like is_bot, is_dm, challenge_response, and dedup_key.
Parse-inbound serves two purposes depending on the App's configuration:
Doorkeeper Mode
When the App has a channel linked to Doorkeeper, parsed messages route to a conversational AI session. The response format focuses on routing: sender, message, is_dm, thread_id.
Trigger Mode
When the App has receive capability and a workflow Trigger is configured for its events, parsed payloads fire workflow triggers instead of (or in addition to) Doorkeeper routing.
For trigger mode, the response must include:
{
"sender": "alice@example.com",
"message": "The email/message body",
"event_type": "email_received",
"thread_id": "unique-thread-id",
"is_bot": false,
"is_dm": true,
"dedup_key": "unique-message-id",
"metadata": {
"subject": "Email subject line",
"gmail_message_id": "19d8f926be07ba29"
}
}
| Field | Required | Description |
|---|---|---|
event_type | Yes | Matches against the Trigger's event_type filter. Use descriptive names like email_received, message_received, issue_opened. |
sender | Yes | Who sent the message. Used for display and routing. |
message | Yes | The body content. For emails, this is the email body. |
metadata | No | Arbitrary key-value pairs rendered in the workflow's initial message. All metadata values are sanitized by TextSanitizer before reaching the LLM. |
is_bot | Yes | Set to true for messages sent by the platform itself. See Feedback Loop Prevention below. |
dedup_key | No | Unique identifier for the message. Prevents duplicate processing when webhooks are retried. |
thread_id | No | Groups related messages (e.g., email thread, chat conversation). |
is_dm | No | Whether the message is a direct/private message. |
Feedback Loop Prevention
When your adapter sends a message on behalf of an agent (via /deliver), the external platform may fire a webhook for that outbound message. Without filtering, this creates a feedback loop: agent sends message -> webhook fires -> trigger starts workflow -> agent sends message -> ...
Your parse-inbound handler must detect bot-sent messages and set is_bot: true. The webhooks controller drops is_bot: true messages before they reach triggers.
How to detect bot-sent messages by platform:
| Platform | Detection Strategy |
|---|---|
| Gmail | Message has SENT label without INBOX label (pure outbound) |
| Slack | Message payload contains a bot_id field |
| Telegram | Message from has is_bot: true |
| Generic webhook | Include a unique header or field when sending, check for it on receive |
# Gmail example: detect outbound-only messages
labels = message_data.get("labelIds", [])
is_bot = "SENT" in labels and "INBOX" not in labels
return {"sender": from_addr, "message": body, "event_type": "email_received", "is_bot": is_bot}
If you skip is_bot detection, a single inbound email can trigger an infinite loop of workflow runs. Always test the feedback loop scenario before deploying a receive-capable adapter.
POST /format (Channel Apps Only)
Format a message for the platform's display conventions (e.g., convert Markdown to Slack's mrkdwn).
POST /sources/list-changes (Optional)
For platforms that back ORQO document sources (Gmail, Google Drive, GitHub-shaped). Returns the set of files that have changed since a previous cursor — used by ORQO's document sync subsystem.
Request body:
{
"credentials": { "access_token": "ya29.a0AfH6SM..." },
"resource_path": "drive/folder-id-or-label",
"cursor": "<opaque cursor from previous call, or null>"
}
Response:
{
"changes": [
{ "path": "report.pdf", "fingerprint": "etag-or-sha", "status": "added" },
{ "path": "old.pdf", "fingerprint": null, "status": "removed" }
],
"cursor": "<new opaque cursor>"
}
status is one of added, modified, or removed. The cursor is opaque to ORQO — your adapter encodes whatever it needs (page token, etag, timestamp).
POST /sources/fetch-file (Optional)
Fetch a single file's bytes for ingestion. Paired with /sources/list-changes.
Request body:
{
"credentials": { "access_token": "ya29.a0AfH6SM..." },
"resource_path": "drive/folder-id-or-label",
"file_path": "report.pdf"
}
Response: the file bytes (binary or base64-encoded JSON, depending on what's natural for the platform).
Authentication
ORQO passes credentials in the request body of every adapter call. The full credential record is included under credentials, with field names mapped per the manifest's maps_to:
{ "credentials": { "access_token": "ya29.a0AfH6SM...", "client_id": "...", "client_secret": "..." } }
For OAuth-backed integrations, ORQO refreshes the access token on its own schedule before invoking the adapter — your handler can assume the token in credentials.access_token is fresh.
Context Headers
ORQO sends context headers with adapter requests that handlers may need:
| Header | Value |
|---|---|
X-Organization-Id | The current organization's ID |
X-Project-Id | The current project's ID (if applicable) |
Important Constraints
- Stateless — Don't store state between requests. Credentials come per-request. If you need to cache, use in-memory caches that can be safely lost on restart.
- Return JSON — All responses must be valid JSON with
Content-Type: application/json. - No content sanitization — Pass through raw content from the external service faithfully. ORQO sanitizes at the platform boundary before content reaches the LLM. Double-sanitization can corrupt legitimate content.
- Channel logic only — The adapter handles webhooks, delivery, formatting, and (optionally) document-source sync. Anything an agent calls directly belongs in a Python tool on the App's Skill, not in the adapter.