Skip to main content
An integration is a third-party app Erdo can act in on your behalf: a database it queries, a warehouse it builds datasets from, a SaaS API an agent or a script calls. Connecting one is the moment its credentials become available to your organization, and it happens in one of two ways. The thing worth internalizing before you start is what decides which way you get. It is not whether the app is one of Erdo’s own native integrations or one of the thousands fronted by our connector platform. It is simply this: do you already hold the credential?
  • You hold it — a database password, an API key, a service-account JSON. There is nobody to send anywhere, so you pass it and the connection exists when the call returns. This is true for native integrations and for SaaS apps whose auth type is keys.
  • You don’t hold it — the app authorizes with OAuth, which means the provider mints the credential during the authorization. There is nothing you could pass, because it does not exist yet. You get back a connect_url, somebody opens it in a browser, and a status check confirms the result.
That asymmetry is why passing credentials to an OAuth app is rejected with an error rather than accepted. An error naming the reason is the only honest answer: the alternative is a call that appears to succeed while storing nothing. All three surfaces — the CLI, the erdo_connect_integration MCP tool, and POST /v1/integrations-connect — run the same code, so everything below behaves identically whichever one you drive.

Maurice order actions

Organizations enabled for the Lead Engine — Proposal skill also get a first-party Maurice action set inside Erdo chat. It is deployment-managed, so there is no customer credential to paste or OAuth connection to create. The agent first calls get_billing_orders, a read-only status action. Maurice returns the developments currently bound to the organization and is the source of truth for order acceptance, payment setup, agreements, invoice periods, payment failures, and proposal-email delivery. The status result returns camelCase projectSlug values and proposalEmail status; the agent passes one as snake-case project_slug when sending an order. A development name or internal id is never used as identity. Current order lines read back serviceLevel, buyerRoleRule, minimumBudgetCents, reachabilityWindowDays, cplCents, and optional brokerCplCents, plus the exact customer-facing termsText frozen into the revision. Legacy orders remain visibly legacy: they return billingBasis as standard_lead or qualified_buyer and no service level. New order writes use only service_level. Status reads are bounded. When Maurice marks order history as truncated, the agent narrows it by status or order id; it never treats a partial list as complete. A truncated development catalogue is surfaced because omitted slugs cannot be guessed. For the current order, agreement.serviceReady reports live-service readiness. When a fresh status response is not exactly true, the proposal workflow says live delivery and spend: blocked; readiness requires an active agreement, ready payment and evidence, and the current UTC date to be inside the accepted start and end dates. Pre-sales research, drafts, and private demo pages can continue outside that window. The status action reports this state but does not itself enforce every later agent or provider mutation path. The actions that change customer billing state always require an approval card:
  • issue_billing_order creates a new order or revision linked to the exact current HTTPS public sales proposal URL, with service dates, an invoice threshold, editable customer-facing terms text, and one of two service levels per development: qualified_lead, or qualified_reachable when immutable reachability evidence is also required. Each line also fixes its minimum budget, buyer-role rule (any, direct_only, broker_only, or priced_by_role), CPL, target, cap behavior, and optional separately billed software service. qualified_reachable requires a 1–30 day reachability window (the proposal workflow recommends 14); other levels omit it. priced_by_role requires a separate broker CPL, while the ordinary CPL is the direct-buyer rate. It returns the private order link; it does not send customer email.
  • email_billing_order sends the one fixed Maurice proposal-and-Order message. It accepts only the orderId and private orderUrl returned by the issue action, and always requires approval. An explicitly approved retry of the same logical email uses the same action and identifiers; it never falls back to generic email.
  • replace_billing_order_link revokes and replaces an unaccepted order’s private bearer link.
  • replace_billing_finance_link replaces the finance setup link for an eligible payment-pending agreement.
  • cancel_billing_payment_pending explicitly cancels a mistaken or abandoned payment-pending agreement.
Erdo injects the authenticated organization and actor on the server. They are not action inputs, so an agent cannot redirect an order to another account or claim another actor. After each approved mutation, the agent reads status again. After a new or revised order is issued, the proposal workflow calls approval-required email_billing_order exactly once. Maurice owns the fixed subject Your Maurice proposal and Order are ready and the sole button Review proposal and accept Order. The private Order link is used only for that button; the body says the Maurice proposal and Order are ready to review in one place. A fresh get_billing_orders call reports proposalEmail status. Generic email is not used and there is no separate proposal email. This proposal workflow runs after brand/research and before public acquisition pages, a live concierge, provider campaigns, or mutating monitoring. It may use private demo screenshots in the sales review, but it publishes only the sales proposal. Erdo does not itself block later live work on an order’s commercial state; a lapsed order is enforced by pausing campaigns and disabling the development’s public surfaces.
The public Erdo sales proposal is linked into the order, but Maurice’s order and finance URLs remain private bearer links. They must never be published on the Erdo page or stored in Knowledge, content artifacts, or analysis/scratch data.

Which organization gets the connection

A connection belongs to one organization, and every surface that starts one names that organization before you commit to it. This matters because the destination is never something you type: it comes from the org the CLI is pinned to, or the workspace you are looking at in the browser, or the X-Organization-ID header your integration sends — and most people at a company using Erdo can reach more than one. A production Stripe account attached to the wrong organization is invisible until something starts syncing there. In the web app, the connectors table says whose it is above the list, and each connect dialog repeats it beside the credential fields. Over the API and the CLI the answer comes back with the result, as organization_name and organization_slug, and the next_step line names it too:
That is the org the request actually carried, not a restatement of what you believed you had pinned — so if it names an organization you did not intend, the connection has not been authorized yet and you can start again with erdo --org <slug> integrations connect <app>. erdo whoami answers in the same shape, so the two agree.

Find the app and how it authorizes

Search the catalog before connecting. The result tells you the identifier to pass and the auth type that decides your flow:
The columns are the app identifier, its source (native or pipedream), its auth types, and its display name. Over REST the same catalog is GET /v1/integration-apps?query=postgres. An empty query lists native integrations plus popular connectable apps. When a native integration and a catalog app share an identifier, the native one wins — so the identifier you pass to connect always resolves the same way it did in apps.

Connect with credentials

CLI

-c takes one key=value per credential field and repeats:
If you run an agency on Erdo, a key like that one is usually yours rather than your clients’ — add --provide-to-managed and every organization you manage uses it as its own, for as long as you manage it. See manager accounts. The field names are the ones the app declares — api_key for a key-auth app, the connection fields for a database. Get one wrong and the connect call fails rather than half-succeeding; for a catalog app the provider’s own message about the offending field is passed straight back to you.

MCP

erdo_connect_integration takes the same thing as a credentials object:

REST

All three answer with the same shape — app, integration_id, status, and a next_step line saying what is actually known:

Reuse a key you already connected somewhere else

A credential is stored so that nothing reads it back through the product, not even the person who supplied it — so when you want the same vendor key in a second organization there is nothing to copy and paste, and the vendor’s own dashboard is the only place to get it again. From CLI 0.85.0, name a connection whose current credential Erdo records you as having supplied, and Erdo copies the credential across for you. It moves inside the service, is re-encrypted under the receiving organization’s own key, and is never returned or displayed:
credentials_from_integration_id is the same thing over REST, and it takes the place of credentials rather than accompanying them:
The new connection is the receiving organization’s own — its own rotation, its own sharing, its own lifetime — so nothing happens to it if the connection it came from is later disconnected. Four things bound it: you must be the person who supplied the credential the source currently stores — whoever connected it, or whoever last rotated it (an organization admin who merely inherited the connection cannot take it elsewhere, a key a recipient entered through a connect link has no supplier on your side and cannot be reused, and a connection Erdo’s own automation made belongs to the organization rather than to anybody), the source must be key-authenticated rather than an OAuth grant, it must be connected and working, and it must be the same app. Deliberately not available to agents: erdo_connect_integration ignores the field, because deciding that a key should leave the organization holding it is a person’s call. Older connections may have no verified supplier record. Erdo refuses reuse in that case, even for the original creator: a different editor may have replaced the key. Connect the destination with a key you supply, or replace the source credential while signed in using erdo integrations connect <app> --org <source-org> --rotate -c '{"api_key":"…"}' before using --from-connection. Renaming or changing settings does not establish credential ownership. Agencies usually want this together with providing the key to the clients they manage.

Verified, or merely stored

The distinction matters when something later fails, so the two cases say different things. A native credential integration is created, exercised against the provider, and activated in the one call. Credentials that don’t work fail the call, and the half-made integration is removed rather than left behind for you to trip over — so status: "active" means Erdo has talked to the provider with those exact credentials. A keys catalog app is different, and its next_step says so plainly: the credentials are stored and the integration is ready to use; they were not validated against the provider — the first action run is what confirms them. The connector platform saves what it is handed without calling the vendor, so a success here means “stored”, not “correct”. Treat the first action you run as the real test.

Connect an OAuth app

Connect it with no credentials. You get status: "pending" and a URL:
Open the URL, authorize, then check status — that call reconciles the connection server-side, so a headless caller finishes the flow without ever touching a browser SDK. Over REST and MCP, return_url sends the person’s browser back to your own page after they authorize instead of Erdo’s data page — worth setting when you are embedding the connect flow in a product of your own. It must be an absolute http(s) URL with no embedded credentials, and it is ignored by apps that connect in one call.
Passing credentials to an OAuth app is an error, not a no-op. The message explains why the app cannot take them — an OAuth provider mints the credential during the authorization, a no-auth app has none — and points you at the flow that does work.

Check status

Over REST: GET /v1/integrations-connect/{app}. Either way you get the app, a connected boolean, the matching integrations, and an optional note counting connections confirmed by this call. “Not connected” is an ordinary answer, not a failure:
This is worth stating because it used to be a 503. Our connector platform does not materialise a record for an end user until that user’s first connection completes, and asking about a user it has never seen answers not found — which is the same fact as “you have connected nothing”, not an outage. Every status check for every catalog app failed that way for anyone starting from zero, which is exactly when you are most likely to be checking. A real provider failure still surfaces as an error, so the signal you want kept its meaning.

Reconnect a connection that has expired

A provider can withdraw a grant at any time — a password change, an administrator revoking app access, a refresh token that quietly aged out — and the connection then reads Needs reauth wherever Erdo shows it. Nothing about the connection is wrong except its credential, so the fix is to authorize it again rather than to remove it and start over. Open Data → Connectors, find the row reading Needs reauth, and press Reconnect. If that connector holds several accounts, expanding the row gives you one Reconnect per account so you repair the right one. The button re-runs the same authorization you did originally against the connection you already have, which is what keeps its datasets, its schedules and its chosen provider account intact — disconnecting and connecting again would leave all of that behind. Where Erdo mentions the problem elsewhere — a page whose data has gone stale, a sync failure notification — it links to the same place. Reconnecting is also how a connection picks up permissions Erdo has since started asking for: the authorization requests the app’s current set of scopes, not the set granted the day you first connected, so the provider shows you the consent screen again and the new permission is granted with the rest. If the person who originally authorized the connection has left, or the credentials belong to someone outside your organization, hand the reconnect to them with a connect link instead of sharing a login. Not every stopped sync needs you, though. A provider answers an access token that expired while a sync job was queued with the same error it uses for a grant you actually revoked, so Erdo asks the provider directly before it decides — and when the answer is that your authorization still stands, the connection is put back to work and its datasets go back on their schedule without anyone pressing anything. A connection reading Connected with a dataset that has stopped syncing is therefore a state that repairs itself within a few minutes; what needs you is a connection that says Needs reauth, which means the provider itself has declined the credential and only a fresh authorization will do.

Rotate an API key

An expired OAuth grant announces itself, but a rotated API key does not. When you issue a new key at the provider and retire the old one, nothing tells Erdo: the connection goes on reading Connected and the first sign of trouble is an action failing. Rotating a key is also something you do on purpose and on a schedule, long before anything breaks — so the control for it does not wait for Erdo to notice. Open Data → Connectors, expand the connector, and press Update credentials on the connection you are rotating. You are taken through the same short form the app showed you when you first connected it, you paste the new key, and the connection takes it. Its identity does not change: everything built on it — the widgets and automations that name it, who in your organization can use it, its datasets — keeps working across the rotation, and Erdo releases the old credential once the new one is in place. Apps that authorize with OAuth show Reconnect in the same spot instead, because there is no key for you to paste. The same thing headlessly, for a connection your organization holds exactly one of:
Without --rotate (rotate_credentials) the same call adds a second connection and leaves the first one holding the dead key — which is why the flag exists rather than the behaviour being inferred. If more than one connection to the app is yours to manage, the call cannot tell which key you rotated and says so; rotate that one from Data → Connectors, where each connection has its own control. Managing a connection — rotating its credentials, changing its sharing, disconnecting it — is open to the person who connected it and to your organization’s admins. Admin reach exists for the day the connector has left the organization: their dead credential must still be rotatable or removable without them. It is management only — being an admin does not make the connection’s data readable through someone else’s credential. As with any credential Erdo stores through the connector platform, a successful rotation means the new key was stored, not that the provider accepts it — the first action run is what confirms that.

A credential the provider has stopped accepting

Needs reauth means the grant between Erdo and the connector platform has lapsed, and Erdo knows the moment it happens. A key retired at the provider’s own end is a different and quieter problem: the connection is intact as far as Erdo can see, so calls keep going out and the provider keeps refusing them one at a time. A connection can be perfectly connected and doing nothing at all. So Erdo now watches what the provider says. When a call comes back rejected because the credential itself was refused — not because a record was missing or a rate limit was hit — the connection is marked in Data → Connectors as Provider rejected credentials, with the date it last happened, and offers the same Reconnect button. GET /v1/integrations-connect/{app}, the check_integration_connection tool and erdo integrations status all report it as provider_auth_failed_at on the connection, alongside a note saying calls will keep failing until it is reauthorized. It is a warning rather than a verdict, and deliberately so: a provider can refuse a perfectly good credential during a brief outage, or refuse a token that expired in the seconds between being issued and being used. Erdo therefore keeps using the connection and keeps everything scheduled against it running, because switching a connection off on the strength of one refusal would break far more than it fixed. The mark clears itself the moment the provider accepts a call again, so a connection you rotate — or one that was only ever having a bad minute — goes quiet on its own. The remedy, when the key really has been retired, is to reconnect with the new one, exactly as above.

Disconnect

Removes a connection by the id erdo integrations list (or GET /v1/integrations) shows. The stored credential is deleted and datasets built on the integration are cleaned up — the same thing removing the connection in the web app does. The response names what was removed (id, app, name), and an id your organization does not hold answers 404.

Set account identity

A connection can carry two non-secret identity ids inside its stored credentials: account_id — which provider account the connection is — and, for Google Ads agency (MCC) connections, login_customer_id — the manager account every request against a client account is sent in the context of. GET /v1/integrations returns both; this endpoint sets them. Most connections never need this. A Google Ads connection settles its login_customer_id the moment an account is chosen for it: an account the picker reached through a manager records that manager, and an account the login reaches directly records itself — see choosing an account under a manager. What this endpoint is for is the connection that has no chosen account yet — a manager-account connection made so that Erdo can create sub-accounts under it, where the id to state is the manager’s own — and connections made before identity stamping existed, which carry no account_id at all. In both cases you state the identity here. At least one field is required; a field you omit is left untouched, so you can set one id without knowing the other. Ids are normalized before storage: spaces and dashes are stripped, because Google renders customer ids as 530-201-2239 while its API wants 5302012239. Only login_customer_id has to be numeric, being a Google Ads manager id; account_id takes whatever shape its provider issues, so a Meta act_1234567890 and a Reddit a2_bzv32cgqp are both stored as given. There are no delete semantics — a wrong id is corrected by writing the right one. Every other credential the connection stores (tokens, expiry) is untouched by this write. Setting account_id on a connection that chooses a provider account also records it as the account that connection operates — one account level, one unambiguous id — so requests actually go there rather than the id being stored and ignored. A provider reached through an owner (a Reddit business, a GA4 account) needs the whole path, so use the connection-scope endpoint for those. The response is the integration’s summary in the same shape GET /v1/integrations returns, carrying the identity now stored — never any token or secret material. An id your organization does not hold answers 404, indistinguishable from an id that never existed.

Apollo, end to end

Apollo is the worked example of a key-authenticated integration: you paste a key and it is connected, with no browser step anywhere in the flow.
The key comes from Apollo under Settings → Integrations → API. Erdo stores it encrypted and sends it to Apollo in the X-Api-Key header, never as a URL parameter — a key in a URL ends up in every log line that records one.
Connecting confirms Apollo is reachable; it cannot confirm your key. Apollo’s key-test endpoint answers 200 even to a key that is not a key at all, so verification has nothing to fail on. The first enrich_person call is what names a bad key, and it says so explicitly when Apollo rejects it.

enrich_person

Apollo has one action. It takes an email address and returns what Apollo knows about that person professionally:
Email only, deliberately. Apollo will also match on a name plus a company domain, but that form guesses — it returns whoever fits best, and you cannot tell a confident match from a plausible one. An address is an identity you already hold for anyone who filled in a form, so the answer is either about that person or about nobody. When Apollo has a record:
When it doesn’t, you get found: false and the address, and nothing else. found is the field to branch on. A miss is a successful answer, not an error — Apollo simply has no record of that address. Because a miss returns no other keys, writing the result straight into a dataset cannot quietly produce a row of empty strings that looks like a person nobody knows anything about. Genuine failures — a rejected key, a rate limit, an Apollo outage — come back as errors, never disguised as found: false. Each successful match costs one Apollo credit. Personal email addresses and phone numbers are never requested and never returned: asking for them turns a one-credit lookup into as many as nine and pulls contact details into Erdo that nobody asked for, so the request omits them and the result has no field to carry them.

Calling it unattended

enrich_person reads and changes nothing — not in Apollo, not in Erdo — so it needs no approval, which is what lets an automation running at 3am use it. A scripted automation or an event pipeline step invokes it directly:
The script never holds the Apollo key — it asks for an enrichment and gets a person back, and the credential stays in Erdo’s encrypted storage. Agents reach the same action through run_integration_action.

Knock CRM, end to end

Knock is the multifamily leasing CRM (part of RealPage) that apartment communities run their guest cards and tours in. Connecting it lets Erdo agents act on your leasing pipeline: push a lead as a guest card, book and cancel tours against real availability, attach notes for the leasing team, and — the reason most orgs connect it — read leasing outcomes straight from the source of truth: a prospect’s status history (NEW → OPEN → BOOKED → VISITED → APPLIED_LEASED / LOST and the stages between), assigned agent, and source, so optimization and reporting grade leads on what actually happened. Connecting takes the two values Knock issues your property-management company — your company id and community id — not a key you generate yourself:
Erdo’s own RealPage vendor key travels with every call on the platform side; property_id is just the default community, and any tool accepts community_id to target a different community of the same company. To look a lead up by email after the desk has worked it:
The agent’s find_knock_prospect answers with the guest card’s current state — status history, agent, source, budget, and preferences — and get_knock_prospect_events reconstructs how it got there. Writes (creating a guest card, booking a tour) raise an approval card first, exactly like every other CRM write. One caveat while the RealPage partner program is in flight: the integration needs Erdo’s own RealPage vendor key, which RealPage issues per vendor. Until it’s live, connect stores your ids but the Knock tools are simply not offered — they appear the moment the key is.

CLI

The erdo integrations commands in context with the rest of the CLI.

MCP

The integration tools an AI assistant drives, and their REST mirrors.

Automations

Scripted work that calls a connected app on a schedule or a trigger.

Data

Turning a connected database or warehouse into a queryable dataset.