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

# Custom Integration

> Build your own custom integration to connect any external service with Cargo.

Custom integrations allow you to extend Cargo's capabilities by connecting to any external service or API. By building a custom integration server, you can create actions, data extractors, and autocomplete endpoints that seamlessly integrate with Cargo's workflows.

<Info>
  Check out the [dummy-integration
  repository](https://github.com/getcargohq/dummy-integration) for a complete
  working example of a custom integration.
</Info>

***

## Overview

A custom integration is an HTTP server that implements a specific API contract. Cargo communicates with your integration server to:

* **Fetch the manifest** — Describes your integration's capabilities (actions, extractors, autocompletes)
* **Authenticate connections** — Validates user credentials when creating a connector
* **Execute actions** — Performs operations in your external service
* **Fetch data** — Pulls data from your service into Cargo data models
* **Provide autocomplete options** — Powers dynamic dropdowns in the Cargo UI

***

## Getting started

A custom integration can be hosted in two ways:

* **As a Cargo Hosting worker** *(recommended)* — write the integration as an edge `fetch(request, env)` handler and let Cargo host it. No infra, no ngrok, no separate domain.
* **As an externally hosted server** — a Node/Express (or any language) server you host yourself. Use this when you need a runtime that isn't supported by workers, or you already have an existing server to wrap.

### Option A — Build with a Cargo Worker (recommended)

#### Step 1: Scaffold the worker

```bash theme={null}
npx @cargo-ai/cli hosting worker init my-integration --template custom-integration
cd my-integration
npm install
```

This drops a TypeScript worker (typed against [`@cargo-ai/worker-sdk`](https://www.npmjs.com/package/@cargo-ai/worker-sdk)) that already implements the Custom Integration HTTP contract:

```
my-integration/
├── manifest.json               # kind + outboundAllowlist (hosts the worker may call)
├── package.json
├── tsconfig.json
└── src/
    ├── index.ts                # default { fetch(request, env) } — routes the contract
    ├── getManifest.ts          # GET /manifest
    ├── authenticate.ts         # POST /authenticate
    ├── listUsers.ts            # POST /listUsers
    ├── completeOauth.ts        # POST /completeOauth
    ├── actions/                # POST /actions/<slug>/execute
    ├── extractors/             # POST /extractors/<slug>/{fetch,count}
    ├── autocompletes/          # POST /autocompletes/<slug>
    └── dynamicSchemas/         # POST /dynamicSchemas/<slug>
```

Edit `src/getManifest.ts` (manifest), `src/authenticate.ts` (credential check), and the per-action / per-extractor handlers under `src/`. Add any external hosts you call to `manifest.json#outboundAllowlist`. Run `npm run type:check` to validate against the typed contract.

#### Step 2: Provision a worker slot, deploy, promote

```bash theme={null}
# Returns <workerUuid>
cargo-ai hosting worker create --name "My Integration" --slug my-integration

# Returns <deploymentUuid>. Deploy the source directory — no local build step.
cargo-ai hosting deployment create --worker-uuid <workerUuid> --source ./

cargo-ai hosting deployment promote --uuid <deploymentUuid>
```

The Cargo Hosting build pipeline runs `npm ci` + `esbuild src/index.ts --bundle --format=esm --platform=neutral --target=es2022` (esbuild transpiles TypeScript natively) to produce the final edge bundle.

#### Step 3: Register the worker as a custom integration

```bash theme={null}
cargo-ai connection custom-integration create \
  --kind worker \
  --worker-uuid <workerUuid>
```

Or declaratively with the CDK:

```ts theme={null}
import { defineCustomIntegration, defineWorker } from "@cargo-ai/cdk";

const worker = defineWorker("my-integration", {
  path: new URL("./my-integration", import.meta.url).pathname,
});

export const integration = defineCustomIntegration("my-integration", {
  worker,
});
```

Cargo will fetch `GET /manifest` from your worker (cached for 5 minutes) and the integration will appear in your workspace's connector catalog.

<Tip>
  Use `cargo-ai hosting worker init x --list-templates` to see all available
  worker templates.
</Tip>

### Option B — Self-hosted external server

Use this path when you can't run on a worker (long-running compute, large native dependencies, an existing Express/Flask/Go service you want to expose, etc.).

#### Step 1: Create your integration server

Start by cloning the dummy integration repository:

```bash theme={null}
git clone https://github.com/getcargohq/dummy-integration.git
cd dummy-integration
npm install
```

Run the development server:

```bash theme={null}
npm run dev
```

Your integration server will start on a local port (e.g., `http://localhost:3000`).

#### Step 2: Expose your local server with ngrok

During development, you can use [ngrok](https://ngrok.com/) to expose your local server to the internet:

```bash theme={null}
ngrok http 3000
```

This will give you a public URL like `https://abc123.ngrok.io` that you can use to register your integration with Cargo.

<Tip>
  For production, deploy your integration server to a cloud provider (AWS, GCP,
  Vercel, Railway, etc.) and use that URL instead.
</Tip>

#### Step 3: Register the external server in Cargo

```bash theme={null}
cargo-ai connection custom-integration create \
  --kind external \
  --base-url https://abc123.ngrok.io
```

Or via raw HTTP:

```bash theme={null}
curl -X POST https://api.getcargo.io/v1/connection/customIntegrations \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "external",
    "baseUrl": "https://abc123.ngrok.io"
  }'
```

Once registered, Cargo will fetch the manifest from your server and the integration will appear in your workspace's connector catalog.

<Warning>
  Your external integration server must be publicly accessible. Cargo's backend
  needs to make HTTP requests to your server's endpoints.
</Warning>

***

## Cargo API for custom integrations

Manage your custom integrations using these API endpoints. All endpoints require authentication.

### Create a custom integration

```
POST /v1/connection/customIntegrations
```

**Request body** — discriminated by `kind`:

```json theme={null}
// Externally hosted server
{
  "kind": "external",
  "baseUrl": "https://your-integration-server.com"
}
```

```json theme={null}
// Cargo Hosting worker-backed
{
  "kind": "worker",
  "workerUuid": "11111111-2222-3333-4444-555555555555"
}
```

**Response:**

```json theme={null}
{
  "customIntegration": {
    "uuid": "ci_abc123",
    "kind": "external",
    "baseUrl": "https://your-integration-server.com",
    "createdAt": "2025-01-01T00:00:00Z"
  }
}
```

### List custom integrations

```
GET /v1/connection/customIntegrations/list
```

**Response:**

```json theme={null}
{
  "customIntegrations": [
    {
      "uuid": "ci_abc123",
      "kind": "external",
      "baseUrl": "https://your-integration-server.com",
      "createdAt": "2025-01-01T00:00:00Z"
    },
    {
      "uuid": "ci_def456",
      "kind": "worker",
      "workerUuid": "11111111-2222-3333-4444-555555555555",
      "createdAt": "2025-01-01T00:00:00Z"
    }
  ]
}
```

### Get a custom integration

```
GET /v1/connection/customIntegrations/:uuid
```

**Response:**

```json theme={null}
{
  "connector": {
    "uuid": "ci_abc123",
    "kind": "external",
    "baseUrl": "https://your-integration-server.com",
    "createdAt": "2025-01-01T00:00:00Z"
  }
}
```

### Update a custom integration

```
PUT /v1/connection/customIntegrations/:uuid
```

**Request body** — pass either `baseUrl` (for `external` integrations) or `workerUuid` (for `worker` integrations); the integration's `kind` cannot change.

```json theme={null}
{
  "baseUrl": "https://new-integration-server.com"
}
```

### Delete a custom integration

```
DELETE /v1/connection/customIntegrations/:uuid
```

***

## Integration server API

Your integration server must implement the following HTTP endpoints:

### GET /manifest

Returns the integration manifest describing all capabilities.

**Response:**

```json theme={null}
{
  "name": "My Integration",
  "description": "A custom integration for my service",
  "icon": "<svg>...</svg>",
  "color": "#6366F1",
  "url": "https://myservice.com",
  "connector": {
    "config": {
      "jsonSchema": {
        "type": "object",
        "properties": {
          "apiKey": {
            "title": "API Key",
            "type": "string"
          }
        },
        "required": ["apiKey"]
      },
      "uiSchema": {}
    },
    "rateLimit": {
      "unit": "minute",
      "max": 60
    }
  },
  "actions": {
    "createRecord": {
      "name": "Create Record",
      "description": "Creates a new record in the service",
      "config": {
        "jsonSchema": {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "type": "string"
            }
          },
          "required": ["name"]
        },
        "uiSchema": {}
      }
    }
  },
  "extractors": {
    "fetchRecords": {
      "name": "Fetch Records",
      "description": "Fetches records from the service",
      "config": {
        "jsonSchema": {},
        "uiSchema": {}
      },
      "mode": {
        "kind": "fetch",
        "isIncremental": false
      },
      "preview": "records"
    }
  },
  "autocompletes": {
    "listOptions": {
      "params": {
        "jsonSchema": {}
      },
      "cacheExpirationInSeconds": 300
    }
  }
}
```

***

### POST /authenticate

Validates the connector configuration (credentials).

**Request body:**

```json theme={null}
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  }
}
```

**Response (success):**

```json theme={null}
{
  "outcome": "success"
}
```

**Response (error):**

```json theme={null}
{
  "outcome": "error",
  "reason": "unauthenticated",
  "errorMessage": "Invalid API key provided"
}
```

***

### POST /listUsers

Lists users available in the connected service (optional).

**Request body:**

```json theme={null}
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  }
}
```

**Response:**

```json theme={null}
[
  {
    "id": "user_123",
    "email": "john@example.com",
    "firstName": "John",
    "lastName": "Doe",
    "profileImage": "https://example.com/avatar.png"
  }
]
```

***

### POST /actions/\[actionSlug]/execute

Executes an action. The `actionSlug` corresponds to a key in the `actions` object returned by your `/manifest` endpoint.

**Request body:**

```json theme={null}
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "actionConfig": {
    "name": "New Record Name"
  }
}
```

**Response (completed):**

```json theme={null}
{
  "outcome": "executed",
  "title": "Record created successfully",
  "data": {
    "id": "rec_123",
    "name": "New Record Name",
    "createdAt": "2025-01-01T00:00:00Z"
  }
}
```

**Response (in progress):**

```json theme={null}
{
  "outcome": "executing"
}
```

***

### POST /extractors/\[extractorSlug]/fetch

Fetches data for a data model extractor.

**Request body:**

```json theme={null}
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "extractorConfig": {},
  "meta": {}
}
```

**Response:**

```json theme={null}
{
  "outcome": "fetched",
  "columns": [
    {
      "slug": "id",
      "type": "string",
      "label": "ID"
    },
    {
      "slug": "name",
      "type": "string",
      "label": "Name"
    },
    {
      "slug": "createdAt",
      "type": "date",
      "label": "Created At"
    }
  ],
  "idColumnSlug": "id",
  "titleColumnSlug": "name",
  "uniqueColumns": [],
  "data": {
    "kind": "records",
    "records": [
      {
        "action": "upsert",
        "override": true,
        "record": {
          "id": "rec_123",
          "name": "Record 1",
          "createdAt": "2025-01-01T00:00:00Z"
        }
      }
    ],
    "hasMore": false
  }
}
```

***

### POST /extractors/\[extractor]/count

Returns the count of records for preview purposes.

**Response:**

```json theme={null}
{
  "count": 150
}
```

***

### POST /autocompletes/\[autocompleteSlug]

Provides options for dynamic dropdowns in the UI.

**Request body:**

```json theme={null}
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "params": {}
}
```

**Response:**

```json theme={null}
{
  "results": [
    {
      "label": "Option 1",
      "value": "option_1",
      "description": "Description for option 1"
    },
    {
      "label": "Option 2",
      "value": "option_2"
    }
  ]
}
```

***

### POST /dynamicSchemas/\[schemaSlug]

Returns dynamic JSON schemas based on runtime parameters.

**Request body:**

```json theme={null}
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "params": {}
}
```

**Response:**

```json theme={null}
{
  "jsonSchema": {
    "type": "object",
    "properties": {
      "dynamicField": {
        "title": "Dynamic Field",
        "type": "string"
      }
    }
  },
  "uiSchema": {}
}
```

***

### POST /completeOauth

Completes OAuth flow for integrations using OAuth authentication.

**Request body:**

```json theme={null}
{
  "code": "oauth_authorization_code",
  "redirectUri": "https://app.getcargo.io/oauth/callback"
}
```

**Response:**

```json theme={null}
{
  "value": "encrypted_oauth_token"
}
```

***

## Using autocompletes in action/extractor forms

To power dynamic dropdowns in your action configuration forms, use the `IntegrationAutocompleteWidget` in your `uiSchema`. This widget calls your `/autocompletes/{slug}` endpoint to fetch options.

### Basic usage

In your `/manifest` endpoint response, include an `autocompletes` entry and reference it in an action's `uiSchema`:

```json theme={null}
{
  "autocompletes": {
    "listWorkspaces": {
      "params": {
        "jsonSchema": {}
      },
      "cacheExpirationInSeconds": 300
    }
  },
  "actions": {
    "syncData": {
      "name": "Sync Data",
      "description": "Sync data from a workspace",
      "config": {
        "jsonSchema": {
          "type": "object",
          "properties": {
            "workspaceId": {
              "type": "string",
              "title": "Workspace"
            }
          },
          "required": ["workspaceId"]
        },
        "uiSchema": {
          "workspaceId": {
            "ui:widget": "IntegrationAutocompleteWidget",
            "ui:options": {
              "slug": "listWorkspaces",
              "params": {}
            }
          }
        }
      }
    }
  }
}
```

### Widget options

| Option         | Type    | Description                                                   |
| -------------- | ------- | ------------------------------------------------------------- |
| `slug`         | string  | The autocomplete slug (matches entry in `/manifest` response) |
| `params`       | object  | Static or dynamic parameters to pass to the endpoint          |
| `allowRefresh` | boolean | Show a refresh button to reload options                       |

### Dynamic parameters

You can reference other form values in `params` using special path expressions:

```json theme={null}
{
  "uiSchema": {
    "accounts": {
      "ui:widget": "IntegrationAutocompleteWidget",
      "ui:options": {
        "slug": "listAccounts",
        "params": {
          "workspaceId": "$this.$parent.workspaceId"
        }
      }
    }
  }
}
```

**Path expressions:**

| Expression | Description                         |
| ---------- | ----------------------------------- |
| `$this`    | Current field value                 |
| `$parent`  | Parent object in the form structure |
| `$root`    | Root of the form data               |

This allows cascading dropdowns where one field's options depend on another field's selected value.

***

## Using dynamic schemas

For fields where the schema depends on runtime values (like user selections), use the `DynamicSchemaWidget`. This widget fetches the JSON schema from your `/dynamicSchemas/{slug}` endpoint.

### Basic usage

In your `/manifest` endpoint response, include a `dynamicSchemas` entry and reference it in an action's `uiSchema`:

```json theme={null}
{
  "dynamicSchemas": {
    "getFieldSchema": {
      "params": {
        "jsonSchema": {
          "type": "object",
          "properties": {
            "objectType": {
              "type": "string"
            }
          }
        }
      }
    }
  },
  "actions": {
    "createRecord": {
      "name": "Create Record",
      "description": "Create a record with dynamic fields",
      "config": {
        "jsonSchema": {
          "type": "object",
          "properties": {
            "objectType": {
              "type": "string",
              "title": "Object Type"
            },
            "fields": {
              "type": "object",
              "title": "Fields"
            }
          }
        },
        "uiSchema": {
          "objectType": {
            "ui:widget": "IntegrationAutocompleteWidget",
            "ui:options": {
              "slug": "listObjectTypes"
            }
          },
          "fields": {
            "ui:widget": "DynamicSchemaWidget",
            "ui:options": {
              "slug": "getFieldSchema",
              "params": {
                "objectType": "$this.$parent.objectType"
              }
            }
          }
        }
      }
    }
  }
}
```

### Widget options

| Option   | Type   | Description                                                     |
| -------- | ------ | --------------------------------------------------------------- |
| `slug`   | string | The dynamic schema slug (matches entry in `/manifest` response) |
| `params` | object | Parameters to pass to the endpoint (supports path expressions)  |

### Endpoint response

Your `/dynamicSchemas/{slug}` endpoint should return both the JSON schema and UI schema for the field:

```json theme={null}
{
  "jsonSchema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "title": "Name"
      },
      "email": {
        "type": "string",
        "title": "Email"
      }
    }
  },
  "uiSchema": {}
}
```

<Tip>
  Dynamic schemas are useful when your integration has different fields per
  object type (e.g., CRM objects like Contacts vs Companies) or when fields are
  user-configurable.
</Tip>

***

## Best practices

<CardGroup cols={2}>
  <Card title="Validate inputs" icon="shield-check">
    Always validate connector and action configurations before processing
    requests.
  </Card>

  <Card title="Handle errors gracefully" icon="triangle-exclamation">
    Return meaningful error messages to help users troubleshoot issues.
  </Card>

  <Card title="Implement rate limiting" icon="gauge">
    Define rate limits in your `/manifest` response to prevent overwhelming your
    service.
  </Card>

  <Card title="Use caching" icon="database">
    Enable caching for autocomplete endpoints to improve performance.
  </Card>
</CardGroup>

***

## Example integration

For a complete working example, see the [Cargo Dummy Integration](https://github.com/getcargohq/dummy-integration) repository on GitHub.

The repository includes:

* Full project structure with TypeScript
* Example `/manifest` endpoint with actions and extractors
* Authentication endpoint implementation
* Action execution handlers
* Development and build scripts
