Skip to main content

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 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.
Check out the dummy-integration repository for a complete working example of a custom integration.

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.

Step 1: Scaffold the worker

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) that already implements the Custom Integration HTTP contract:
my-integration/
├── manifest.json               # outboundAllowlist (hosts the worker may call)
├── package.json
├── tsconfig.json
├── scripts/
│   └── copy-runtime-files.mjs  # copies manifest.json + package.json into dist/
└── 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, build, deploy, promote

# Returns <workerUuid>
cargo-ai hosting worker create --name "My Integration" --slug my-integration

# Compiles src/ → dist/ and copies manifest.json + package.json + package-lock.json into dist/
npm run build

# Returns <deploymentUuid>. Note the `--source ./dist` — that's the compiled bundle.
cargo-ai hosting deployment create --worker-uuid <workerUuid> --source ./dist

cargo-ai hosting deployment promote --uuid <deploymentUuid>
The Cargo Hosting build pipeline runs npm ci + esbuild dist/index.js --bundle --format=esm --platform=neutral --target=es2022 to produce the final edge bundle.

Step 3: Register the worker as a custom integration

cargo-ai connection custom-integration create \
  --kind worker \
  --worker-uuid <workerUuid>
Cargo will fetch GET /manifest from your worker (cached for 5 minutes) and the integration will appear in your workspace’s connector catalog.
Use cargo-ai hosting worker init x --list-templates to see all available worker templates.

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:
git clone https://github.com/getcargohq/dummy-integration.git
cd dummy-integration
npm install
Run the development server:
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 to expose your local server to the internet:
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.
For production, deploy your integration server to a cloud provider (AWS, GCP, Vercel, Railway, etc.) and use that URL instead.

Step 3: Register the external server in Cargo

cargo-ai connection custom-integration create \
  --kind external \
  --base-url https://abc123.ngrok.io
Or via raw HTTP:
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.
Your external integration server must be publicly accessible. Cargo’s backend needs to make HTTP requests to your server’s endpoints.

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:
// Externally hosted server
{
  "kind": "external",
  "baseUrl": "https://your-integration-server.com"
}
// Cargo Hosting worker-backed
{
  "kind": "worker",
  "workerUuid": "11111111-2222-3333-4444-555555555555"
}
Response:
{
  "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:
{
  "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:
{
  "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.
{
  "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:
{
  "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:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  }
}
Response (success):
{
  "outcome": "success"
}
Response (error):
{
  "outcome": "error",
  "reason": "unauthenticated",
  "errorMessage": "Invalid API key provided"
}

POST /listUsers

Lists users available in the connected service (optional). Request body:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  }
}
Response:
[
  {
    "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:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "actionConfig": {
    "name": "New Record Name"
  }
}
Response (completed):
{
  "outcome": "executed",
  "title": "Record created successfully",
  "data": {
    "id": "rec_123",
    "name": "New Record Name",
    "createdAt": "2025-01-01T00:00:00Z"
  }
}
Response (in progress):
{
  "outcome": "executing"
}

POST /extractors/[extractorSlug]/fetch

Fetches data for a data model extractor. Request body:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "extractorConfig": {},
  "meta": {}
}
Response:
{
  "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:
{
  "count": 150
}

POST /autocompletes/[autocompleteSlug]

Provides options for dynamic dropdowns in the UI. Request body:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "params": {}
}
Response:
{
  "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:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "params": {}
}
Response:
{
  "jsonSchema": {
    "type": "object",
    "properties": {
      "dynamicField": {
        "title": "Dynamic Field",
        "type": "string"
      }
    }
  },
  "uiSchema": {}
}

POST /completeOauth

Completes OAuth flow for integrations using OAuth authentication. Request body:
{
  "code": "oauth_authorization_code",
  "redirectUri": "https://app.getcargo.io/oauth/callback"
}
Response:
{
  "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:
{
  "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

OptionTypeDescription
slugstringThe autocomplete slug (matches entry in /manifest response)
paramsobjectStatic or dynamic parameters to pass to the endpoint
allowRefreshbooleanShow a refresh button to reload options

Dynamic parameters

You can reference other form values in params using special path expressions:
{
  "uiSchema": {
    "accounts": {
      "ui:widget": "IntegrationAutocompleteWidget",
      "ui:options": {
        "slug": "listAccounts",
        "params": {
          "workspaceId": "$this.$parent.workspaceId"
        }
      }
    }
  }
}
Path expressions:
ExpressionDescription
$thisCurrent field value
$parentParent object in the form structure
$rootRoot 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:
{
  "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

OptionTypeDescription
slugstringThe dynamic schema slug (matches entry in /manifest response)
paramsobjectParameters 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:
{
  "jsonSchema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "title": "Name"
      },
      "email": {
        "type": "string",
        "title": "Email"
      }
    }
  },
  "uiSchema": {}
}
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.

Best practices

Validate inputs

Always validate connector and action configurations before processing requests.

Handle errors gracefully

Return meaningful error messages to help users troubleshoot issues.

Implement rate limiting

Define rate limits in your /manifest response to prevent overwhelming your service.

Use caching

Enable caching for autocomplete endpoints to improve performance.

Example integration

For a complete working example, see the Cargo 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