Siphondocs
Guides

Add a connector

Go from an API's docs to a working, published connector spec.

This walks through authoring a connector for a paginated REST API with a login exchange and a bit of enrichment — the shape most vendor APIs take.

Three ways to start

New connector (/connectors/new) offers three entry points, all ending in the visual builder where you review, test, and publish:

  • Draft with AI — paste a sample API response and Spec Assist drafts the record path, id, transform, and output columns for you.
  • Import spec — paste an existing connector spec as YAML or JSON; it's validated, then opens in the builder.
  • Start from scratch — configure everything by hand.

The rest of this guide shows the spec each of those produces.

1. Declare identity and credentials

Start with metadata and the connection params. Mark anything sensitive secret.

apiVersion: siphon/v1
kind: ConnectorSpec
metadata: { key: acme-crm, name: Acme CRM, version: 1.0.0 }
connectionParams:
  - { key: baseUrl, type: url, required: true, default: "https://app.acme.com" }
  - { key: email, type: string, required: true, secret: true }
  - { key: password, type: string, required: true, secret: true }

2. Wire auth

If the vendor has a login endpoint rather than a static key, use custom_token_exchange — Siphon caches the token and re-mints it on expiry.

auth:
  strategy: custom_token_exchange
  request:
    method: POST
    url: "{{ params.baseUrl }}/api/Auth/login"
    headers: { content-type: application/json }
    body: { email: "{{ secrets.email }}", password: "{{ secrets.password }}" }
  tokenPath: "$.accessToken"
  expiresInPath: "$.expiresIn"
  inject: { type: header, name: Authorization, format: "Bearer {{ token }}" }

3. Fetch and paginate a resource

defaults:
  baseUrl: "{{ params.baseUrl }}"
  headers: { Accept: application/json }
resources:
  - key: deals
    canonicalEntity: Deal
    sync: { mode: full }
    fetch:
      id: list
      path: /api/Deals
      pagination:
        strategy: page
        pageSize: 100
        pageParam: PageNumber
        sizeParam: PageSize
        stopWhen: { path: "$.hasNextPage", equals: false }
      extract: { recordsPath: "$.items", idPath: "$.id", updatedAtPath: "$.createdAt" }

4. Enrich (optional)

If the list is thin, fan out detail calls. Here the list has no clientId, so it takes a two-hop join.

    enrich:
      - { id: detail, dependsOn: [list], path: "/api/Deals/{{ record.id }}", extract: { dataPath: "$" }, assignTo: _detail, onError: continue }
      - { id: client, dependsOn: [detail], when: "record._detail.clientId", path: "/api/Clients/{{ record._detail.clientId }}", extract: { dataPath: "$" }, assignTo: _client, onError: continue }

5. Transform to canonical fields

    transform:
      engine: jsonata
      expression: |
        {
          "externalId": $string(id),
          "DEAL_ID": $string(id),
          "CLIENT_NAME": clientName,
          "CLIENT_EMAIL": _client.email,
          "CREATED_AT_UTC": createdAt
        }
    validate: { required: [externalId], onInvalid: quarantine }
    output:
      fields:
        - { name: DEAL_ID, type: string }
        - { name: CLIENT_NAME, type: string }
        - { name: CLIENT_EMAIL, type: string }
        - { name: CREATED_AT_UTC, type: timestamp }

6. Choose a change-detection strategy

  • API can filter by updatedSince? Use sync.mode: incremental and template the watermark into the query.
  • API can't filter, records have a timestamp? filter: client, changeDetection: watermark.
  • API can't filter, no timestamp? filter: client, changeDetection: hash with a scoped hashFields.

See Sync & change filtering.

7. Validate, test, publish

pnpm siphon validate acme-crm.yaml
pnpm siphon run acme-crm.yaml --connection conn.json   # smoke-test live

Then publish the version (immutable) and onboard a connection. Author transforms and preview them live in the Connector Builder — no YAML required.

On this page