Skip to main content
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

FlagDescription
--config-path(Required) Path to the YAML configuration file
--validate-config-onlyValidate the configuration file and exit without running
--enable-command-actionsEnable command actions (shell script execution)
--client-idConductorOne client ID for service mode
--client-secretConductorOne 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

FieldRequiredDescription
versionYesSchema version, must be "1"
app_nameYesApplication name for this connector
app_descriptionNoDescription of the application
varsNoGlobal variables available throughout configuration
connectYesAPI connection and authentication settings
httpNoHTTP client settings (timeouts, retries)
resource_typesYesResource type definitions (minimum 1 required)
error_handlingNoGlobal error handling configuration
actionsNoCustom 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

auth:
  type: none

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

FieldRequiredDescription
nameYesHuman-readable name
descriptionNoDescription of this resource type
traitsNoTraits for this type (user, group, role, app)
depends_onNoResource types that must be processed first
parent_typeNoParent resource type for hierarchical resources
child_typesNoChild resource types
listYes*How to list resources (*unless using static_resources)
static_resourcesNoStatically defined resources
static_entitlementsNoPredefined entitlements
entitlementsNoDynamic entitlement configuration
grantsNoGrant discovery configuration
skip_entitlements_and_grantsNoSkip 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

VariableDescription
.resource.idCurrent resource ID
.resource.display_nameCurrent resource display name
.parent_resource.idParent resource ID
.principal.idPrincipal ID (for provisioning)
.principal.traits.user.emailPrincipal’s email
.pre_requests.<name>.bodyResponse from a pre-request

Pagination

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
pagination:
  strategy: link
  next_link_path: links.next   # Path to next URL in response

No pagination

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

1
In ConductorOne, navigate to Connectors > Add connector.
2
Search for Baton and click Add.
3
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
4
Set the owner for this connector and click Next.
5
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

Pagination issues

  • 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.