REST API
Erdo’s REST API lets you integrate your data platform into any application. Query datasets, write data, manage conversations, create automations, and more — all via standard HTTP requests. Base URL:https://api.erdo.ai
OpenAPI spec: the API Reference section in the sidebar is generated
from the full machine-readable OpenAPI spec — every /v1 endpoint with its
request and response schemas. Point a code generator or a coding agent at the
spec to build a typed client.
Authentication: All requests require a Bearer token in the Authorization header.
Getting an API Key
Click your profile in the bottom-left corner of Erdo and go to API Keys. Create a new key and copy the token — it’s shown only once. To replace a key, choose Rotate beside it and confirm. The old key stops working immediately, and Erdo shows the replacement secret only once. Copy it and update every application that uses the key. If you no longer need a key, choose Revoke instead. Rotation keeps the key’s owner, name, default organization, access mode, and scope. Its original lifetime starts again from the time of rotation, capped at 36,500 days (100 years). For example, rotating a 30-day key gives the replacement 30 days from rotation; it does not carry over only the remaining days. You can also rotate withPOST /v1/tokens/{id}/rotate. Only the key’s owner can
rotate it, and a scoped or read-only API key cannot initiate rotation. The
response includes the new token and replaced_token_id; store the new token
when it is returned because it cannot be read again.
An admin of a member’s home organization sees that member’s keys and can
revoke one, but cannot rotate it. A replacement acts as its owner — in every
organization the owner belongs to, including ones the admin cannot reach — so
handing the admin a working secret would widen their access. Revoke the key and
let its owner create the replacement.
An unscoped API key is an account-level credential: it acts as you, and
works in any organization you’re a member of. The organization stored on the
key is only its default org — the one used when a request doesn’t name one.
It is not a hard scope. Scoped API keys have explicit
capability and resource limits.
To use an unscoped key in a specific organization, send the X-Organization-ID
header with the org’s id or slug. The backend validates on every request that
you’re a member of that org, so a key cannot reach an org you don’t belong to:
erdo token create --name ci,
erdo token list, erdo token rotate <id>, erdo token revoke <id>. Set a command’s org with
erdo --org acme <command> or change the active default with erdo org use acme.
Project context
Projects are optional work context inside an organization. To run a request in one, send its slug or its UUID asX-Project-ID together with the organization
header:
GET /v1/projects (or erdo project list) and pass the
same context to the CLI with erdo --org acme --project <slug> <command>.
Bounded outreach is stricter: every consent, batch, recipient, and
action request requires X-Project-ID. It never falls back to organization-wide
scope because consent, suppression, provider receipts, and retries must stay in
one authenticated project. The key’s user must also be a persisted owner or
admin of the active organization.
Scoped API Keys
A server that only needs part of the API should not hold a key that can do everything. Mint a scoped API key with a capability allowlist (e.g.datasets:query) and optional dataset/workstream slugs — every
endpoint outside its grants is denied, and no capability can mint keys, touch
managed organizations, or connect/delete integrations.
Scoped Tokens
For building apps where your end-users interact with Erdo, use scoped tokens to restrict access to specific datasets and threads.API Surface
API keys and scoped tokens work on the documented API surface: every/v1
endpoint, the /mcp endpoint, and the endpoints used by the published SDKs
(scoped-token minting, thread messaging, agent invocation, dataset reads).
Requests to any other path return 403 permission_denied — other routes are
internal to the Erdo app and not a stable contract to build against. If you
need a capability that isn’t on /v1 yet, tell us rather than coupling to an
internal route.
Errors
A failed request answers with a JSON body carrying a machine-readablecode and
a message written to be acted on:
Agent-facing messages name the next tool or endpoint to call where one exists,
so a coding agent driving the API can usually recover from a 4xx without a human
reading the docs.
Datasets
List Datasets
Each thread includes its canonical title, creator ID, creator name and email
when that user is still in the organization, and its creation source. New
threads without an explicit name are titled from their first user message.
Configure an Integration Dataset
enable_sync materializes sync-capable integrations through Erdo’s canonical data platform; it does not create a snapshot dataset.
Create Dataset
The response may carry
warnings — advisory findings such as plural/singular near-misses of an existing purpose. Creating a second dataset for an established purpose is refused (not warned): the error names the dataset to reuse.
List Dataset Purposes
declared_schema. Every org starts with the platform-provisioned entries (leads, page events, widget events, widget leads — present when the org uses each kind), and the vocabulary grows as you assign purposes to new datasets.
datasets lists every dataset currently carrying the purpose, ordered by slug, even when dataset_count is greater than one and no single dataset_slug is returned. For a dataset synced from a connected integration, integration_config_key names its provider; it is empty otherwise, and also when the integration belongs to a different organization than the dataset.
dataset_slug is the current write target to reuse, and declared_schema is its producer contract. The established_by_* fields remain for audit and compatibility; they identify the dataset that first introduced the vocabulary entry and can differ after a purpose moves.
Add ?scope=managed from a manager account to read the same vocabulary across every client organization it operates. Each entry then carries organization_slug and organization_name, and its established_by_dataset_slug is organization-qualified — so you can query a client’s dataset directly without switching organizations:
scope=managed is for the manager organization’s admins and owners — the same people who can switch into a client. Anybody else gets a permission error naming the requirement, rather than an empty list that reads like there is nothing there. An organization that manages no clients gets an empty purposes array and a message saying so, rather than an empty answer that reads like a failed query.
Ensure a Purpose Dataset
preferred_dataset when you already have an authoritative binding. The declared schema is stored before a newly created dataset receives its purpose, and concurrent calls for one purpose converge on one dataset.
created, the resolved dataset identity, and its actual declared_schema. A purpose carried by multiple datasets, a populated legacy dataset with no declaration, or an incompatible contract returns a conflict naming the migration required; the endpoint never silently chooses one.
When a producer has reviewed a contract change, reviewed_optional_additions may list optional column names from declared_schema. On the existing unique purpose-bound file dataset, Erdo validates the complete contract and adds only those missing, typed columns while holding the purpose, ownership, and dataset-write locks. Unknown, required, duplicate, undeclared, or invalidly typed names are refused before mutation. The default ensure request remains strict; this field does not authorize adoption of a split, repurposed, or unrelated dataset.
For the leads purpose, a newly created dataset gets permanent lead identity at creation. Add email_column and phone_column (default email/phone) to name which declared columns hold the contact evidence the identity resolver reads — at least one must exist in your contract, or the request is refused rather than capturing nothing. The returned declared_schema also carries the platform-owned canonical_lead_id key column, which you never declare or write; ensuring the same purpose again with your original contract stays compatible.
Upload a File as a Dataset
ready reports whether schema extraction succeeded; when false, the file
stored but is not yet queryable (a warning explains why).
Delete Dataset
Search Datasets
Get Dataset Schema
Query Dataset (SQL)
VALUES clause. Store those rows in a dataset first. Column names, quoted text and comments containing VALUES remain ordinary SQL text; validation uses the storage provider’s comment and string rules.
For file datasets (CSV/Excel), the table is always named
data regardless of
the resource key. For database, warehouse, and synchronized API integration
datasets, use the actual resource or table name from the schema — passing
resource_key selects which resource to query, and that resource’s name is the
SQL table name.Query several datasets at once
POST /v1/datasets/:slug/query would run it: the same access check, default filters,
filter_names, resource_key and limit.
Results come back in the order you sent them. A read that fails carries its own error
and the others still answer, so one missing dataset does not blank the whole screen.
Scoped API keys can’t call this endpoint. They are confined to named datasets, so use
POST /v1/datasets/:slug/query with them instead.
Query Dataset (Natural Language)
rows holds the values in columns order — the same tabular shape
/v1/datasets/:slug/fetch returns, so both dataset reads are consumed the same
way — and output is that result rendered to read. row_count is how many rows
the query matched and can exceed the number of rows returned, which is capped at
1000; compare the two to tell whether you have the whole result. When the
dataset’s saved filters shaped the read, applied_filters names them, so a count
is never reported as the whole truth when it excludes something. On failure,
success is false and error says why.
Fetch Dataset Contents
Get Dataset Context
Writing Data
Write data into your datasets from any application. Rows are written to whatever storage backend the dataset uses (Postgres, ClickHouse, or CSV file storage).Write Rows
Append rows (no key column):
rows_inserted and rows_updated split rows_affected into the rows that created a row in the
dataset and the rows that replaced one. A keyed write is an upsert, so this is the only way to tell
a new record from a correction to one you already had — an arrival timestamp cannot say, because an
upsert rewrites the row it matched and the timestamp moves with it. Rows also carry
erdo_created_at, set when a write creates the row and left alone by every write after, for when
you come back to the dataset later rather than reading the response.
A keyed write in the default replace mode rewrites the whole row, so a column you leave out is
blanked. To change some columns of a row and keep the rest, write in merge mode and name the columns
you are changing:
merge.
Storage-specific behavior:
Delete Rows
Update Schema
Each operation object:
Ask Questions
Ask a Data Question
allow_writes is true. The agent does not create datasets, write declarations, schedule syncs, or persist other changes. When an answer needs something that does not exist yet, it describes what it would create. A saved shortcut (saved_ask_id) runs its stored configuration, including any writes that configuration performs.
Integrations
Connect third-party apps and data sources so Erdo can use them — in agent runs, data questions, and datasets. What decides the flow is whether you already hold the credential: databases, API keys, and service accounts connect in a single call, and so do SaaS apps whose auth type iskeys. OAuth apps are the exception, because the provider mints their credentials during the authorization itself — they return a connect_url for the user to authorize in a browser, and polling the status endpoint completes the connection. Connecting integrations covers both flows end to end.
List Integrations
Search Connectable Apps
auth_types field tells you how connection works — database, api_key, service_account, basic, and aws_iam on a native integration, plus keys on a SaaS app, all connect directly with credentials, while oauth/oauth2/oauth1 apps go through a browser authorization step.
Connect an Integration
Connecting a database — created, verified, and activated in one call:
connect_url to open in a browser. Pass return_url when you embed the flow in your own product so the user’s browser lands back on your page (not Erdo’s) after authorizing:
keys app connects the same way but is not verified: its credentials go to the connector platform, which stores what it is given without calling the vendor. The response says so in next_step, and the first action run is what confirms the key.
Check Connection Status
connect_url.
An app you have never connected answers {"app": "...", "connected": false, "integrations": []}. That is an ordinary answer about your account, not an error, so a caller starting from zero can poll it safely; a genuine provider failure still comes back as an error.
Delete an Integration
404 Not Found.
Discover Tables
:integration is the app key (e.g. postgres) or, when several instances of the same app are connected, the integration id. Without schema_name it lists the selectable schemas (all SQL databases and warehouses). With schema_name it lists that schema’s tables with columns, types, and row estimates — supported for SQL databases (Postgres, MySQL, and compatible); warehouses (BigQuery, Snowflake, ClickHouse) list schemas but not per-table columns here.
Create a Dataset from an Integration
Threads & Conversations
List Threads
Get Thread Messages
content_type and, when present,
ui_content_type. Use ui_content_type to select the presentation component for
structured content such as tool results and generated UI. UI-generation items
also include created_by_invocation_id when the renderer needs the originating
invocation to load referenced data.
Create Thread
Delete Thread
agents:run capability.
Send Message
context is available to the agent for this turn without becoming part of the
visible user message in the thread. This lets an application supply the current
page, selection, or other structured state while preserving an honest transcript.
The persisted message is attributed to the authenticated user, including when
the request comes through an application using the REST or MCP surface.
Memories & Skills
Memories store reusable knowledge and instructions that Erdo’s AI uses in future conversations.Create Memory
Search Memories
List Memories
Delete Memory
Artifacts
Artifacts are AI-generated outputs from agent runs — insights, metrics, alerts, and tables.List Artifacts
Get Artifact
Screenshots
Capture a public web page to a PNG and get a signed, time-limited download URL. Only works on URLs reachable without login (a marketing site, a published Erdo page athttps://pages.erdo.ai/p/{id}, etc.).
signed_url, bucket_key, media_type, width, height, expires_at.
Pages
Deploy authenticated, data-wired HTML apps to Erdo over HTTP — the same surface as theerdo_deploy_page MCP tools, for CI and scripts. See the Build Apps on Erdo guide for the page runtime, the window.erdo client, and the read/write model.
Deploy Page
Returns the page
id, editor url, optional public_url, and structured validation results. A deploy with validation errors still saves — fix them and iterate.
Update Page
js to fix a script without resending html/css. Omit any field to keep its current value. build_brief updates only the page judges’ stored brief; an empty string clears it without changing the page content or other judge metadata. artifact_kind (landing_page, lead_followup_page, dashboard or app) re-declares what the page is for, in the same write as the content; omit it to keep the current purpose. Meta and LinkedIn tags load only on lead-generation landing pages, so set it whenever a rewrite changes the page’s job. dataset_slugs replaces the current grants; public toggles visibility. Returns fresh validation results.
Validate Page
window.erdo usage, and dataset-slug references. Accepts html (required), css, js, runtime, and dataset_slugs.
Clone Page
clone_from_page_id instead of content. The content is copied byte for byte, and the lead-form event pipelines the source page owns are duplicated onto the copy with the pipeline ids in its content rewritten to match — so the copy captures its own leads instead of filing them against the page it was copied from. The copy is private unless the request sets public: true; the source’s publish state is never inherited. Leave html/css/js/runtime and the grant lists empty — the copy carries the source’s.
Returns the usual deploy response plus
source_page_id, cloned_pipelines (one entry per duplicated pipeline: source_id, id, purpose, slug), rewrites (every id substitution applied to the copied content), and notes.

