Baton-HTTP is a configuration-driven connector that lets you write YAML instead of Go code. Instead of implementing the ResourceSyncer interface, you describe how to map an API to ConductorOne’s resource model. Your ops team can own integrations directly — no engineering queue.
Resources
When to use baton-http
Use baton-http when:
- The target system has a REST API
- You need a quick integration without writing Go
- The access model maps to user/group/resource patterns
- You want non-developers to maintain the integration
Use a custom connector when:
- The API requires authentication not supported by baton-http (e.g., Kerberos, SAML)
- You need heavy data transformation or business logic
- You need maximum performance optimization
Command-line options
| Flag | Description |
|---|
--config-path | (Required) Path to the YAML configuration file |
--validate-config-only | Validate the configuration file and exit without running |
--enable-command-actions | Enable command actions (shell script execution) |
--client-id | ConductorOne client ID for service mode |
--client-secret | ConductorOne client secret for service mode |
Authentication credentials for the target API are configured in the YAML file using environment variable interpolation (e.g., ${API_TOKEN}), not via command-line flags.
Configuration structure
Every baton-http configuration file must include these core elements:
version: "1" # Required, must be "1"
app_name: "Your Application" # Required
app_description: "Optional description"
connect:
base_url: "https://api.example.com/v1"
auth:
type: "bearer"
token: "${API_TOKEN}"
resource_types:
user:
# Resource configuration...
Top-level fields
| Field | Required | Description |
|---|
version | Yes | Schema version, must be "1" |
app_name | Yes | Application name for this connector |
app_description | No | Description of the application |
vars | No | Global variables available throughout configuration |
connect | Yes | API connection and authentication settings |
http | No | HTTP client settings (timeouts, retries) |
resource_types | Yes | Resource type definitions (minimum 1 required) |
error_handling | No | Global error handling configuration |
actions | No | Custom action definitions |
Connection configuration
The connect section defines how to connect to your HTTP API:
connect:
base_url: "https://api.example.com/v1"
auth:
type: "bearer"
token: "${API_TOKEN}"
request_defaults:
content_type: "application/json"
headers:
Accept: "application/json"
query_params:
limit: "100"
pagination:
strategy: "offset"
limit_param: "limit"
offset_param: "offset"
page_size: 25
Authentication methods
Baton-HTTP supports multiple authentication methods. Configure authentication under connect.auth.
No authentication
Bearer token
auth:
type: bearer
token: "${API_TOKEN}"
Basic authentication
auth:
type: basic
username: "${API_USERNAME}"
password: "${API_PASSWORD}"
API key
auth:
type: api_key
api_key:
header: "X-API-Key" # Header name
prefix: "ApiKey" # Optional prefix
key: "${API_KEY}" # The key value
OAuth2 client credentials
auth:
type: oauth2_client_credentials
oauth2_client_credentials:
token_url: "https://api.example.com/oauth/token"
# Or use OIDC discovery:
# issuer: "https://login.example.com"
client_id: "${CLIENT_ID}"
client_secret: "${CLIENT_SECRET}"
scope: "read:users read:groups"
token_expiry_padding: 60 # Refresh 60 seconds before expiry
OAuth2 password (ROPC)
auth:
type: oauth2_password
oauth2_password:
token_url: "https://api.example.com/oauth/token"
client_id: "${CLIENT_ID}"
client_secret: "${CLIENT_SECRET}"
username: "${USERNAME}"
password: "${PASSWORD}"
scope: "api"
Bearer dynamic
Use for APIs that issue tokens via a login endpoint:
auth:
type: bearer_dynamic
bearer_dynamic:
token_url: "https://api.example.com/login"
username: "${USERNAME}"
password: "${PASSWORD}"
token_field: "access_token" # JSON field containing token
expiry_field: "expires_at" # Optional, auto-parses JWT if omitted
token_expiry_padding: 60
Resource type configuration
Resource types define the entities you want to sync. Each resource type specifies how to list resources and map API responses to ConductorOne resources.
Basic structure
resource_types:
user:
name: "User"
description: "User accounts"
traits:
- user
list:
request:
method: GET
url: /users
response:
items_path: items
mapping_type: resource
resource_mapping:
id: cel:item.id
display_name: cel:item.name
user_traits:
email: cel:item.email
login: cel:item.username
status: 'cel:item.active ? "enabled" : "disabled"'
skip_entitlements_and_grants: true
Resource type fields
| Field | Required | Description |
|---|
name | Yes | Human-readable name |
description | No | Description of this resource type |
traits | No | Traits for this type (user, group, role, app) |
depends_on | No | Resource types that must be processed first |
parent_type | No | Parent resource type for hierarchical resources |
child_types | No | Child resource types |
list | Yes* | How to list resources (*unless using static_resources) |
static_resources | No | Statically defined resources |
static_entitlements | No | Predefined entitlements |
entitlements | No | Dynamic entitlement configuration |
grants | No | Grant discovery configuration |
skip_entitlements_and_grants | No | Skip entitlement/grant processing |
Data mapping with CEL expressions
Baton-HTTP uses Common Expression Language (CEL) for data mapping. CEL expressions are prefixed with cel:.
Response mapping
response:
items_path: data.users # Path to array in response
mapping_type: resource # resource, grant, or entitlement
resource_mapping:
id: cel:item.id
display_name: cel:item.firstName + " " + item.lastName
description: cel:item.email
user_traits:
email: cel:item.email
login: cel:item.username
status: 'cel:item.status == "active" ? "enabled" : "disabled"'
profile:
first_name: cel:item.firstName
last_name: cel:item.lastName
department: cel:item.department
Common CEL patterns
# Conditional expressions
status: 'cel:item.active ? "enabled" : "disabled"'
# String concatenation
display_name: cel:item.firstName + " " + item.lastName
# Null-safe access with has()
parent_id: "cel:has(item.parent.id) ? item.parent.id : null"
# Array access
primary_email: cel:item.emails[0].address
URL templates
Use Go template syntax (prefixed with tmpl:) for dynamic URLs:
# URL templating
url: tmpl:/groups/{{.resource.id}}/members
# With parent resource
url: tmpl:/orgs/{{.parent_resource.id}}/teams/{{.resource.id}}
# Request body templating
body: |
tmpl:{
"user_id": "{{.principal.id}}",
"role": "member"
}
Available template variables
| Variable | Description |
|---|
.resource.id | Current resource ID |
.resource.display_name | Current resource display name |
.parent_resource.id | Parent resource ID |
.principal.id | Principal ID (for provisioning) |
.principal.traits.user.email | Principal’s email |
.pre_requests.<name>.body | Response from a pre-request |
Configure pagination under connect.pagination (global) or per-resource under list.pagination.
Offset-based
pagination:
strategy: offset
limit_param: limit
offset_param: offset
page_size: 100
total_path: totalResults # Optional: path to total count
Cursor-based
pagination:
strategy: cursor
limit_param: limit
cursor_param: cursor
cursor_path: meta.nextCursor # Path to next cursor in response
page_size: 100
Page-based
pagination:
strategy: page
limit_param: per_page
page_param: page
page_start: 1 # Starting page number
page_size: 100
Link-based
pagination:
strategy: link
next_link_path: links.next # Path to next URL in response
pagination:
strategy: none
Entitlements
Entitlements define permissions that can be granted to resources.
Static entitlements
Use for predefined entitlements like group membership:
static_entitlements:
- id: member
display_name: cel:resource.display_name + " Member"
description: cel:"Member of " + resource.display_name
slug: member
purpose: assignment # assignment, permission, or role
grantable_to:
- user
Dynamic entitlements
Fetch entitlements from an API:
entitlements:
- request:
url: tmpl:/spaces/{{.resource.id}}/permissions
response:
items_path: items
mapping_type: entitlement
entitlement_mapping:
id: cel:resource.id + "-" + item.operation
display_name: cel:"Can " + item.operation + " on " + resource.display_name
slug: cel:item.operation
purpose: permission
grantable_to:
- user
- group
Grants
Grants define which principals have which entitlements:
grants:
- request:
url: tmpl:/groups/{{.resource.id}}/members
response:
items_path: members
mapping_type: grant
grant_mapping:
principal_id: cel:item.userId
principal_type: user
entitlement_name: member
Conditional grants
Use skip_if to filter grants:
grants:
- request:
url: tmpl:/resources/{{.resource.id}}/permissions
response:
items_path: items
skip_if: cel:item.subject.type == "anonymous"
mapping_type: grant
grant_mapping:
principal_id: cel:item.subject.id
principal_type: cel:item.subject.type
entitlement_name: cel:item.permission
Provisioning
Enable provisioning to grant and revoke access through ConductorOne.
Grant and revoke
static_entitlements:
- id: member
display_name: "Group Member"
purpose: assignment
grantable_to:
- user
provisioning:
grant:
request:
method: POST
url: tmpl:/groups/{{.resource.id}}/members
body: |
tmpl:{
"user_id": "{{.principal.id}}"
}
success_condition: cel:response.status_code == 201
revoke:
request:
method: DELETE
url: tmpl:/groups/{{.resource.id}}/members/{{.principal.id}}
success_condition: cel:response.status_code == 204
Pre-requests
Use pre-requests when you need data from another API call:
provisioning:
grant:
request:
pre_requests:
user_details:
url: tmpl:/users/{{.principal.id}}
method: GET
method: PUT
url: tmpl:/groups/{{.resource.id}}/members/{{.pre_requests.user_details.body.username}}
HTTP client settings
Configure timeouts and retries under the http section:
http:
timeout: 30s
headers:
Accept: "application/json"
retry:
max_attempts: 3
initial_backoff: 1s
max_backoff: 10s
Error handling
Configure error handling globally or per-request:
error_handling:
error_status_codes:
"429":
action: retry
retry_after: 30s
max_retries: 3
"404":
action: warn
message: "Resource not found"
"500":
action: retry
retry_after: 5s
Error actions: fail (default), retry, warn, ignore
Running baton-http
Validate configuration
baton-http --config-path ./config.yaml --validate-config-only
One-shot mode (local testing)
baton-http --config-path ./config.yaml -f sync.c1z
baton resources -f sync.c1z
baton grants -f sync.c1z
Service mode (production)
baton-http --config-path ./config.yaml \
--client-id "$C1_CLIENT_ID" \
--client-secret "$C1_CLIENT_SECRET"
Deploying to Kubernetes
Step 1: Set up a new connector in ConductorOne
In ConductorOne, navigate to Connectors > Add connector.
Search for Baton and click Add.
Choose how to set up the new connector:
- Add the connector to a currently unmanaged app
- Add the connector to a managed app
- Create a new managed app
Set the owner for this connector and click Next.
In the Settings area, click Edit, then click Rotate to generate a new Client ID and Secret. Save these credentials.
Step 2: Create Kubernetes configuration
Secret
apiVersion: v1
kind: Secret
metadata:
name: baton-http-secrets
type: Opaque
stringData:
C1_CLIENT_ID: <ConductorOne client ID>
C1_CLIENT_SECRET: <ConductorOne client secret>
API_TOKEN: <Your API token>
ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: baton-http-config
data:
config.yaml: |
version: "1"
app_name: My Application
connect:
base_url: "https://api.example.com/v1"
auth:
type: bearer
token: "${API_TOKEN}"
resource_types:
user:
name: User
traits:
- user
list:
request:
url: /users
response:
items_path: data
mapping_type: resource
resource_mapping:
id: cel:item.id
display_name: cel:item.name
user_traits:
email: cel:item.email
status: 'cel:item.active ? "enabled" : "disabled"'
skip_entitlements_and_grants: true
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: baton-http
spec:
selector:
matchLabels:
app: baton-http
template:
metadata:
labels:
app: baton-http
spec:
containers:
- name: baton-http
image: ghcr.io/conductorone/baton-http:latest
args:
- "--config-path=/config/config.yaml"
envFrom:
- secretRef:
name: baton-http-secrets
volumeMounts:
- name: config
mountPath: /config
volumes:
- name: config
configMap:
name: baton-http-config
Step 3: Deploy
Apply the configuration files to your Kubernetes cluster and verify the connector appears in ConductorOne under Applications > Managed apps.
Example: GitHub integration
version: "1"
app_name: Github
app_description: Github Organization
connect:
base_url: https://api.github.com
auth:
type: bearer
token: "${GITHUB_TOKEN}"
request_defaults:
headers:
Accept: application/json
query_params:
per_page: 100
resource_types:
org:
name: Organization
traits:
- app
child_types:
- user
- team
list:
request:
url: /user/orgs
response:
items_path: items
mapping_type: resource
resource_mapping:
id: cel:item.id
display_name: cel:item.login
user:
name: User
depends_on:
- org
parent_type: org
traits:
- user
list:
request:
url: tmpl:/orgs/{{.parent_resource.id}}/members
response:
items_path: items
mapping_type: resource
resource_mapping:
id: cel:item.login
display_name: cel:item.login
user_traits:
login: cel:item.login
skip_entitlements_and_grants: true
team:
name: Team
depends_on:
- org
parent_type: org
traits:
- group
list:
request:
url: tmpl:/orgs/{{.parent_resource.id}}/teams
response:
items_path: items
mapping_type: resource
resource_mapping:
id: cel:item.id
display_name: cel:item.name
group_traits:
profile:
description: cel:item.description
static_entitlements:
- id: member
display_name: "Team Member"
purpose: assignment
grantable_to:
- user
grants:
- request:
url: tmpl:/orgs/{{.parent_resource.id}}/team/{{.resource.id}}/members
response:
items_path: items
mapping_type: grant
grant_mapping:
principal_id: cel:item.login
principal_type: user
entitlement_name: member
Example: OAuth2 with provisioning
version: "1"
app_name: ConductorOne
app_description: ConductorOne API Integration
connect:
base_url: https://${C1_ENDPOINT}/api/v1
auth:
type: oauth2_client_credentials
oauth2_client_credentials:
token_url: https://${C1_ENDPOINT}/auth/v1/token
client_id: "${C1_CLIENT_ID}"
client_secret: "${C1_CLIENT_SECRET}"
request_defaults:
headers:
Accept: application/json
resource_types:
user:
name: User
traits:
- user
list:
request:
url: /users
response:
items_path: list
mapping_type: resource
resource_mapping:
id: cel:item.user.id
display_name: cel:item.user.displayName
user_traits:
login: cel:item.user.username
email: cel:item.user.email
skip_entitlements_and_grants: true
Troubleshooting
Authentication errors
- Verify credentials are correct and environment variables are set
- Check token hasn’t expired
- For OAuth2, verify token URL and scopes
Resources not syncing
- Use
--validate-config-only to check configuration
- Verify
items_path matches your API response structure
- Check CEL expressions with sample data
- Confirm pagination
strategy matches your API
- Verify parameter names (
limit_param, offset_param, etc.)
- Check
cursor_path or next_link_path for cursor/link pagination
Mapping errors
- Test CEL expressions against sample API responses
- Use
has() for optional fields to avoid null errors
- Check for typos in field names
For more information, see the official download center.