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.
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
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:
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:
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 in Cargo via API
Custom integrations are registered using the Cargo API. You’ll need an API token.
Create a custom integration:
curl -X POST https://api.getcargo.io/v1/connection/customIntegrations \
-H "Authorization: Basic YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"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 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:
{
"baseUrl" : "https://your-integration-server.com"
}
Response:
{
"customIntegration" : {
"uuid" : "ci_abc123" ,
"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" ,
"baseUrl" : "https://your-integration-server.com" ,
"createdAt" : "2025-01-01T00:00:00Z"
}
]
}
Get a custom integration
GET /v1/connection/customIntegrations/:uuid
Response:
{
"customIntegration" : {
"uuid" : "ci_abc123" ,
"baseUrl" : "https://your-integration-server.com" ,
"createdAt" : "2025-01-01T00:00:00Z"
}
}
Update a custom integration
PUT /v1/connection/customIntegrations/:uuid
Request body:
{
"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):
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" : "[email protected] " ,
"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"
}
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
}
}
Returns the count of records for preview purposes.
Response:
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"
}
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" : {}
}
}
}
}
}
}
}
Option Type Description slugstring The autocomplete slug (matches entry in /manifest response) paramsobject Static or dynamic parameters to pass to the endpoint allowRefreshboolean Show 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:
Expression Description $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"
}
}
}
}
}
}
}
}
Option Type Description slugstring The dynamic schema slug (matches entry in /manifest response) paramsobject 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:
{
"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