> ## Documentation Index
> Fetch the complete documentation index at: https://docs.erdo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Connecting integrations

> Connect a third-party app to Erdo from the CLI, an MCP client, or the API — one call when you hold the credential, a browser authorization when the provider mints it.

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.

## 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:

```bash theme={null}
erdo integrations apps postgres
# postgres  native      database  PostgreSQL

erdo integrations apps slack
# slack     pipedream   oauth     Slack
```

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`.

| Auth type                                                    | Source      | How it connects                                                                                                                                                            |
| ------------------------------------------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `database`, `api_key`, `service_account`, `basic`, `aws_iam` | native      | Pass credentials. Erdo verifies them against the provider and activates the integration in the same call.                                                                  |
| `keys`                                                       | catalog app | Pass credentials. They are stored with the connector platform and the connection is active immediately — but nothing calls the vendor, so they are **not** validated here. |
| `oauth`, `oauth2`, `oauth1`                                  | either      | No credentials. You get a `connect_url` to open in a browser.                                                                                                              |
| `none`                                                       | native      | No credentials at all — it connects on the spot.                                                                                                                           |

## Connect with credentials

### CLI

`-c` takes one `key=value` per credential field and repeats:

```bash theme={null}
erdo integrations connect postgres \
  -c host=db.example.com -c port=5432 -c database=analytics \
  -c username=readonly -c password=secret \
  -n "Production DB"

erdo integrations connect apollo -c api_key=$APOLLO_KEY -n Apollo
```

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:

```json theme={null}
{
  "app": "apollo",
  "name": "Apollo",
  "credentials": { "api_key": "abc123" }
}
```

### REST

```bash theme={null}
curl -X POST "https://api.erdo.ai/v1/integrations-connect" \
  -H "Authorization: Bearer $ERDO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "app": "apollo",
        "name": "Apollo",
        "credentials": {"api_key": "abc123"}
      }'
```

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

```json theme={null}
{
  "app": "apollo",
  "integration_id": "0f2c…",
  "status": "active",
  "next_step": "The integration is connected and ready to use."
}
```

### 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:

```bash theme={null}
erdo integrations connect slack
# ... connect_url ...
# Open this URL in a browser to authorize, then run: erdo integrations status slack
```

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.

<Warning>
  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.
</Warning>

## Check status

```bash theme={null}
erdo integrations status apollo
erdo integrations list          # everything connected: app, status, auth type, name
```

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:

```json theme={null}
{ "app": "slack", "connected": false, "integrations": [] }
```

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.

## 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.

```bash theme={null}
erdo integrations connect apollo -c api_key=$APOLLO_KEY -n Apollo
erdo integrations status apollo
```

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.

<Note>
  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.
</Note>

### enrich\_person

Apollo has one action. It takes an email address and returns what Apollo knows
about that person professionally:

```json theme={null}
{ "email": "ada@example.com" }
```

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:

```json theme={null}
{
  "found": true,
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "title": "VP Engineering",
  "linkedin_url": "https://linkedin.com/in/…",
  "city": "London",
  "state": "",
  "country": "United Kingdom",
  "organization": "Analytical Engines Ltd",
  "organization_website": "https://example.com"
}
```

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](/automations#agent-or-script) or an
[event pipeline](/event-pipelines) step invokes it directly:

```js theme={null}
const person = actions.invoke("apollo", "enrich_person", { email });
if (person.found) {
  // person.title, person.organization, person.linkedin_url …
}
```

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`.

## Related

<CardGroup cols={2}>
  <Card title="CLI" href="/cli">
    The `erdo integrations` commands in context with the rest of the CLI.
  </Card>

  <Card title="MCP" href="/mcp/overview">
    The integration tools an AI assistant drives, and their REST mirrors.
  </Card>

  <Card title="Automations" href="/automations">
    Scripted work that calls a connected app on a schedule or a trigger.
  </Card>

  <Card title="Data" href="/data">
    Turning a connected database or warehouse into a queryable dataset.
  </Card>
</CardGroup>
