> For the complete documentation index, see [llms.txt](https://docs.edisonscientific.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.edisonscientific.com/edison-client/custom-integrations.md).

# Custom Integrations

{% hint style="info" %}

### Custom integrations are only available for enterprise clients at this time. Please reach out to <sales@edisonscientific.com> for commercial inquiries.&#x20;

{% endhint %}

### Overview

An **integration** gives an agent authenticated access to an external API — the mechanism behind the built-in Notion, Slack, Teams, Google Drive, and GitHub connections. This page covers registering **your own** API as a first-class integration, using that same interface.

The agent never accesses your credential. Keys are stored in a secret manager, and the sandbox's egress proxy injects them into outbound requests in transit. The key is not in the agent's environment at all — so it cannot be exfiltrated by a prompt injection or leaked in a transcript.

### How it works

```
your agent ──HTTP──► network sidecar ──► your API
                          │
                          └── looks up the provider by domain,
                              injects the credential from Secret Manager
```

1. A **provider** entry in the integrations catalog declares the API: its domains, how the credential is presented, and a URL to validate a key against.
2. That entry drives the sandbox proxy's TLS-intercept domain list and its credential-injection rules.
3. A **connection** attaches an actual credential at **user** or **organization** scope.
4. At request time, resolution prefers the calling user's own credential and falls back to the organization's.

### 1. Register the provider

```python
from edison_client import EdisonClient

client = EdisonClient()

glyph = client.create_custom_integration(
    CustomIntegrationSpec(
        name="Glyph Structure Recognition",
        domains=["glyph.ml.internal.example.com"],
        auth_method=IntegrationAuthMethod.BEARER,
        auth_flow=IntegrationAuthFlow.STATIC_KEY,
        header_name="Authorization",
        validation_url="https://glyph.ml.internal.example.com/v1/models",
        instructions=(
            "Paste a Glyph API token from the ML platform console. "
            "Read scope on /v1/ocsr and /v1/markush is sufficient."
        ),
        image="https://cdn.example.com/glyph-logo.png",
    ),
    organization_id=42,  # omit to publish tenant-wide
)
glyph.id  # UUID used by every call below
```

Registering creates the catalog entry only — no credential is stored yet.

#### `CustomIntegrationSpec`

| Field                     | Type                    | Required             | Meaning                                                                                      |
| ------------------------- | ----------------------- | -------------------- | -------------------------------------------------------------------------------------------- |
| `name`                    | `str`                   | yes                  | Display name. **Unique across the catalog.**                                                 |
| `domains`                 | `list[str]`             | yes                  | Hosts the proxy intercepts and injects credentials for. Bare hostnames — no scheme, no path. |
| `auth_method`             | `IntegrationAuthMethod` | yes                  | How the credential is presented (below)                                                      |
| `auth_flow`               | `IntegrationAuthFlow`   | no (`static_key`)    | How the credential is obtained                                                               |
| `header_name`             | `str`                   | no (`Authorization`) | Header the credential is injected into; for `query_param`, the parameter name                |
| `instructions`            | `str`                   | yes                  | Shown in the connect dialog: where a user gets a key                                         |
| `image`                   | `str`                   | yes                  | Logo URL for the UI tile                                                                     |
| `validation_url`          | `str \| None`           | no                   | Cheap authenticated `GET` used to prove a stored key works                                   |
| `oauth_authorization_url` | `str \| None`           | OAuth only           | Authorization endpoint                                                                       |
| `oauth_token_url`         | `str \| None`           | OAuth only           | Token endpoint                                                                               |
| `oauth_scopes`            | `list[str] \| None`     | OAuth only           | Requested scopes                                                                             |

`IntegrationAuthMethod` — how the credential rides on the request:

| Value         | Result                        |
| ------------- | ----------------------------- |
| `bearer`      | `Authorization: Bearer <key>` |
| `header`      | `<header_name>: <key>`        |
| `query_param` | `?<param_name>=<key>`         |

> **`query_param` is declarable but not yet injectable.** The credential lookup path raises `NotImplementedError` for it. Use `bearer` or `header` until that lands.

`IntegrationAuthFlow`:

| Value        | Meaning                                    |
| ------------ | ------------------------------------------ |
| `static_key` | A long-lived key pasted by a user or admin |
| `oauth2`     | Authorize → callback → refresh loop        |

### 2. Attach a credential

Organization scope — admin only, and by default all member's agents inherit it:

```python
client.connect_integration(glyph.id, api_key="glyph_sk_org_...", organization_id=42)
```

User scope — personal, and takes priority over the org credential for that user's agents:

```python
client.connect_integration(glyph.id, api_key="glyph_sk_user_...")
```

| Parameter             | Type               | Meaning                                             |
| --------------------- | ------------------ | --------------------------------------------------- |
| `provider_catalog_id` | `UUID`             | Provider to connect                                 |
| `api_key`             | `str`              | Your key for that API                               |
| `organization_id`     | `int \| None`      | `None` → user scope; set → org scope (admin)        |
| `expires_at`          | `datetime \| None` | Optional expiry, surfaced in the UI before it bites |

The key goes straight to the secret manager; only a secret reference is written to the backend.

### 3. Validate before anyone depends on it

```python
client.validate_integration(glyph.id)
# IntegrationValidation(provider='Glyph Structure Recognition', valid=True, error=None)
```

Re-probes the stored credential against `validation_url`. Runs automatically on connect, and on demand thereafter — the cheapest way to distinguish "the integration is broken" from "the agent did something wrong".

### 4. Use it

Nothing further is required of the agent or of you. The agent calls the API normally, with no credential anywhere in its code:

```python
# inside the agent's sandbox — note the absence of any token
resp = await httpx.AsyncClient(timeout=180).post(
    "https://glyph.ml.internal.example.com/v1/markush/predictions",
    json={"image_base64": img, "output_format": "both"},
)
resp.json()["cxsmiles"]
```

In practice you would pair the integration with a skill — a markdown guide telling the agent which endpoints exist, how to choose between them, and how to report results.

To guarantee an agent always has that guidance rather than discovering it, name the skill in a profile's `auto_skills` and deploy that profile as your own job:

```python
profile = Profile(
    base_image_tier=BaseImageTier.CHEM,
    packages=["e14c-core", "e14c-client", "e14c-data-storage", "e14c-chemistry"],
    auto_skills=["glyph-structure-recognition"],
)
client.create_job(JobDeploymentConfig(
    name="acme-assay-analysis", profile=profile, environment_variables=ENV_VARS
))
```

The integration handles access; the skill handles judgment; the profile guarantees the skill is loaded. See configuration.md.

### 5. Rotate and offboard

Rotation can be achieved by overwriting, and offboarding via a revoke method.

```python
client.connect_integration(glyph.id, api_key="glyph_sk_rotated_...")
client.revoke_integration(glyph.id, organization_id=42)
```

`revoke_integration` disconnects the provider and deletes the stored credential.

### Listing what is available

```python
for provider in client.list_integrations():
    print(provider.name, provider.connected, provider.last_used)

# Org-level connection status instead of the caller's — requires org admin
client.list_integrations(organization_id=42)
```

#### `IntegrationProvider`

| Field            | Type                  | Meaning                                                |
| ---------------- | --------------------- | ------------------------------------------------------ |
| `id`             | `UUID`                | Catalog ID used by connect / validate / revoke         |
| `name`           | `str`                 | Display name                                           |
| `domains`        | `list[str]`           | Intercepted hosts                                      |
| `auth_flow`      | `IntegrationAuthFlow` | `static_key` or `oauth2`                               |
| `connected`      | `bool`                | Whether a usable credential is attached for the caller |
| `org_managed`    | `bool`                | Credential comes from the organization, not the user   |
| `last_validated` | `datetime \| None`    | Last successful validation                             |
| `last_used`      | `datetime \| None`    | Last agent request that used it                        |
| `expires_at`     | `datetime \| None`    | Credential expiry, if declared                         |

#### `IntegrationConnection`

| Field                 | Type                                          |
| --------------------- | --------------------------------------------- |
| `provider`            | `str`                                         |
| `provider_catalog_id` | `UUID`                                        |
| `scope`               | `IntegrationScope` — `user` or `organization` |
| `connected`           | `bool`                                        |

#### `IntegrationValidation`

| Field      | Type          |
| ---------- | ------------- |
| `provider` | `str`         |
| `valid`    | `bool`        |
| `error`    | `str \| None` |

### Constraints worth knowing up front

* Provider `name` is globally unique in the catalog.
* A new domain becomes interceptable only after the sandbox proxy configuration regenerates. This happens on an infrastructure level within minutes but is not instant.
* `query_param` credentials are not injectable yet (see above).
* User credentials override org credentials.
* Providers can be mutually exclusive. The catalog supports a mutex group, so at most one provider in a group may be connected per user — relevant if you register several entries for the same underlying service.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.edisonscientific.com/edison-client/custom-integrations.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
