Skip to main content
Switchbord’s compatibility layer lets Charles-era automations and partner systems migrate to the control plane without requiring simultaneous upstream rewrites. Two families of endpoints are available: staged aliases under /compat/* for new integrations, and Charles-style legacy paths that match the original URL structure exactly. Both families normalize payloads to the same internal contracts and enqueue the same outbox jobs.
This layer is a controlled migration boundary, not the permanent architecture. Compatibility endpoints are rate-limited to 100 requests per minute per IP address.

Contact upsert

POST /compat/contact

Upsert a contact record using the Charles-compatible contact update shape. Switchbord syncs the contact to the backing store and returns an operation summary.
curl -X POST https://api.your-switchbord.example.com/compat/contact \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15551234567",
    "external_reference_id": "crm_contact_001",
    "source_type": "opt_in",
    "person_properties": {
      "name": "Jane Smith",
      "email": "jane@example.com",
      "locale": "en_US"
    }
  }'
Request body:
phone_number
string
required
E.164 phone number for the contact. Must be at least one character.
external_reference_id
string
Your system’s identifier for this contact. Used to correlate records across systems.
source_type
string
The origin or intent of this update. Defaults to "internal". Pass "opt_out" to set the contact’s subscriber status to blocked; any other value sets it to subscribed.
person_properties
object
Arbitrary key-value attributes. Switchbord reads name, email, and locale from this object and maps them to first-class contact fields. All other keys are preserved.
Responses:
accepted
boolean
true when the contact was successfully synced.
mode
string
Always "compatibility" for this endpoint.
operation
string
"created" or "updated" depending on whether this is a new or existing contact.
contact
object
The synced contact record with its assigned ID and normalized fields.
requestId
string
A UUID for tracing this request in logs and audit entries.
{
  "accepted": true,
  "mode": "compatibility",
  "operation": "created",
  "contact": {
    "id": "ctr_01hx...",
    "phoneNumber": "+15551234567",
    "name": "Jane Smith",
    "email": "jane@example.com",
    "externalReferenceId": "crm_contact_001"
  },
  "source": "opt_in",
  "receivedAt": "2025-01-15T10:30:00.000Z",
  "requestId": "9f3a2b1c-..."
}
StatusCondition
200Contact synced successfully
400Validation failure — phone_number missing or malformed
429Rate limit exceeded (100 req/min per IP)

PUT /api/v1beta/contact/

Legacy Charles-compatible path with identical behavior to POST /compat/contact. The response shape matches the original Charles API for drop-in compatibility.
curl -X PUT https://api.your-switchbord.example.com/api/v1beta/contact/ \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15551234567",
    "source_type": "opt_in",
    "person_properties": { "name": "Jane Smith" }
  }'
Legacy response shape:
{
  "status": "success",
  "message": "Contact updated successfully.",
  "requestId": "9f3a2b1c-...",
  "contactId": "ctr_01hx...",
  "operation": "created"
}
Validation failure (legacy shape):
{
  "status": "error",
  "message": "Contact update payload is invalid.",
  "errors": {
    "fieldErrors": { "phone_number": ["Required"] },
    "formErrors": []
  }
}

Journey trigger

POST /compat/journey-trigger/

Trigger a journey using the compat-mode trigger contract. The legacyPath captures any path segments after the prefix and is used as the deduplication key for the outbox job.
curl -X POST "https://api.your-switchbord.example.com/compat/journey-trigger/welcome-flow" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15551234567",
    "flow_id": "welcome_flow_v2",
    "payload": {
      "promo_code": "SUMMER25",
      "language": "en"
    }
  }'
legacyPath
string
required
One or more path segments that identify the journey. These are captured as a wildcard and used to route and deduplicate the trigger job.
Request body:
phone_number
string
required
E.164 phone number for the contact to enroll in the journey.
flow_id
string
Optional flow identifier. Used in deduplication keys when provided.
trigger_id
string
Optional trigger identifier. Takes precedence over flow_id in deduplication keys.
payload
object
Arbitrary key-value personalization data forwarded into the queued job payload.
Response:
{
  "accepted": true,
  "mode": "compatibility",
  "triggerEventId": "evt_01hx...",
  "outboxJobId": "job_01hx...",
  "route": "welcome-flow",
  "trigger": {
    "phone_number": "+15551234567",
    "flow_id": "welcome_flow_v2",
    "payload": { "promo_code": "SUMMER25" }
  },
  "receivedAt": "2025-01-15T10:30:00.000Z",
  "requestId": "4e7f1a2b-..."
}

POST /webhooks/v0/rest-trigger/organization//flow//trigger/

Legacy Charles-compatible journey trigger. The organizationId and flowId path parameters are captured and forwarded into the routed job payload.
curl -X POST "https://api.your-switchbord.example.com/webhooks/v0/rest-trigger/organization/org_123/flow/welcome_flow/trigger/" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15551234567",
    "payload": { "promo_code": "SUMMER25" }
  }'
organizationId
string
required
Your organization identifier. Captured and forwarded in the job payload.
flowId
string
required
The flow to trigger. Captured and forwarded in the job payload.
Legacy response shape:
{
  "status": "success",
  "message": "Triggered flow successfully.",
  "requestId": "4e7f1a2b-...",
  "triggerEventId": "a1b2c3d4-...",
  "outboxJobId": "job_01hx..."
}

POST /webhooks/v0/rest-trigger/organization//flow//trigger//

Identical to the path above but includes an explicit triggerId parameter. Use this when your Charles-era automation passes a trigger identifier.
curl -X POST "https://api.your-switchbord.example.com/webhooks/v0/rest-trigger/organization/org_123/flow/welcome_flow/trigger/trig_456/" \
  -H "Content-Type: application/json" \
  -d '{ "phone_number": "+15551234567" }'
triggerId
string
required
An explicit trigger identifier. Used in the deduplication key for the outbox job.

Emarsys custom events

POST /api/v0/webhooks/incoming/emarsys/custom_external_event

Ingest a custom external event from Emarsys. Switchbord normalizes both the flattened and nested Emarsys contact payload styles, syncs the contact if a phone number or external reference ID is present, records a webhook event, and enqueues an integration.emarsys.custom_event outbox job.
curl -X POST "https://api.your-switchbord.example.com/api/v0/webhooks/incoming/emarsys/custom_external_event?secret=your-emarsys-secret" \
  -H "Content-Type: application/json" \
  -d '{
    "event_name": "purchase_completed",
    "phone": "+15551234567",
    "external_reference_id": "crm_001",
    "event_data": {
      "order_id": "ORD-9876",
      "amount": 120.00
    }
  }'

Authentication

When EMARSYS_WEBHOOK_SECRET is set in your environment, Switchbord validates the secret query parameter against it. Requests with a missing or incorrect secret receive a 401 response.
secret
string
The shared secret value. Required when EMARSYS_WEBHOOK_SECRET is configured.

Request body — flat style

event_name
string
required
The name of the event. Must not contain whitespace.
phone
string
E.164 phone number for the contact.
external_reference_id
string
Your system’s identifier for the contact.
email
string
Email address for the contact.
event_data
object
Arbitrary key-value payload forwarded into the outbox job.
voucher_pool
string
Optional voucher pool name, forwarded into the outbox job.

Request body — nested style

Switchbord also accepts nested contact payloads with contact or contact_data objects:
{
  "event_name": "purchase_completed",
  "contact": {
    "phone": "+15551234567",
    "external_reference_id": "crm_001"
  },
  "event_data": {
    "order_id": "ORD-9876"
  }
}
Response:
{
  "status": "success",
  "message": "Event received and processed successfully.",
  "requestId": "8a2b3c4d-...",
  "webhookEventId": "evt_01hx...",
  "outboxJobId": "job_01hx..."
}
StatusCondition
200Event accepted and queued
400Validation failure — event_name missing or contains whitespace
401Secret validation failed