CoPulse API Reference

Integrations API

External integrations

Send outbound messages and phone calls, create CRM contacts/accounts/deals with custom properties, or kick pre-built workflows from your CRM or automation platform. Authenticate with a project-scoped API key. No dashboard JWT required.

Authentication

Create an API key in Setup → Integrations → Developer API. Keys are project-scoped. Send the full secret once at creation as a Bearer token on every request.

Authorization: Bearer cp_live_<lookup>_<secret>

Responses use { success, data, error }. Production base URL:

https://copulse.app/api/v1/integrations/

OpenAPI spec: openapi.yaml

API key scopes

ScopeAllows
outbound:messagePOST outbound messages (WhatsApp, SMS, RCS, email, web)
outbound:callPOST outbound phone calls
outbound:workflowPOST workflow kickoffs (automation-first)
crm:writePOST contacts, accounts, and deals (with custom properties)
triggers:readPoll request status (also allowed with outbound scopes)

Send an outbound message

POST /api/v1/integrations/outbound/messages

Scope outbound:message · Returns 202 Accepted

curl -sS -X POST "https://copulse.app/api/v1/integrations/outbound/messages" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "hubspot-deal-991-whatsapp",
    "channel": "whatsapp",
    "to": { "phone": "+919876543210", "external_id": "hs-contact-42" },
    "template": {
      "id": "YOUR_WHATSAPP_LIBRARY_TEMPLATE_ID",
      "variables": ["Priya", "Skyline Towers", "Saturday 11am"]
    },
    "contact": {
      "name": "Priya Sharma",
      "external_id": "hs-contact-42",
      "properties": { "first_name": "Priya", "tier": "gold" }
    },
    "options": { "property_merge_mode": "fill_blank" }
  }'

Response

{
  "success": true,
  "data": {
    "request_id": "uuid",
    "event_id": "uuid",
    "customer_id": "uuid",
    "conversation_id": "uuid",
    "status": "accepted"
  },
  "error": null
}

Save request_id for status polling. Prefer template.id (library template UUID) with ordered template.variables for WhatsApp, SMS, and RCS template sends. SMS always requires an approved library template. WhatsApp and RCS may also send free-form text (WhatsApp only inside an open customer-care session window). Values in template.variables may be literals or {{variable}} placeholders resolved from the trigger snapshot, CRM account properties, then contact columns.

WhatsApp free-form text

curl -sS -X POST "https://copulse.app/api/v1/integrations/outbound/messages" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "hubspot-deal-991-whatsapp-text",
    "channel": "whatsapp",
    "to": { "phone": "+919876543210" },
    "text": "Hi {{first_name}}, your site visit is confirmed.",
    "contact": { "name": "Priya Sharma", "properties": { "first_name": "Priya" } }
  }'

SMS

curl -sS -X POST "https://copulse.app/api/v1/integrations/outbound/messages" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "hubspot-deal-991-sms",
    "channel": "sms",
    "to": { "phone": "+919876543210" },
    "template": {
      "id": "YOUR_SMS_LIBRARY_TEMPLATE_ID",
      "variables": ["Priya", "Skyline Towers"]
    },
    "contact": { "name": "Priya Sharma", "properties": { "first_name": "Priya" } }
  }'

RCS

curl -sS -X POST "https://copulse.app/api/v1/integrations/outbound/messages" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "hubspot-deal-991-rcs",
    "channel": "rcs",
    "to": { "phone": "+919876543210" },
    "template": {
      "id": "YOUR_RCS_LIBRARY_TEMPLATE_ID",
      "variables": ["Priya"]
    },
    "contact": { "name": "Priya Sharma", "properties": { "first_name": "Priya" } }
  }'

Email

curl -sS -X POST "https://copulse.app/api/v1/integrations/outbound/messages" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "hubspot-deal-991-email",
    "channel": "email",
    "to": { "email": "priya@example.com", "external_id": "hs-contact-42" },
    "text": "Hi Priya, your site visit is confirmed.",
    "contact": { "name": "Priya Sharma", "properties": { "first_name": "Priya" } }
  }'

Web chat

curl -sS -X POST "https://copulse.app/api/v1/integrations/outbound/messages" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "crm-web-991",
    "channel": "web",
    "to": { "customer_id": "CONTACT_UUID" },
    "text": "Your agent left a note on this conversation.",
    "contact": { "name": "Priya Sharma" }
  }'

to.customer_id or to.external_id is required for the web channel.

Place an outbound call

POST /api/v1/integrations/outbound/calls

Scope outbound:call · Returns 202 Accepted

curl -sS -X POST "https://copulse.app/api/v1/integrations/outbound/calls" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "hubspot-deal-991-call",
    "to": { "phone": "+919876543210", "external_id": "hs-contact-42" },
    "template": {
      "id": "YOUR_PHONE_LIBRARY_TEMPLATE_ID",
      "variables": ["Priya", "Saturday 11am"]
    },
    "contact": {
      "name": "Priya Sharma",
      "properties": { "first_name": "Priya", "project": "Skyline Towers" }
    }
  }'

Alternatively pass opening_message (plain text) instead of template. Wallet funds are reserved before the call is queued.

Trigger a workflow

POST /api/v1/integrations/outbound/trigger

Scope outbound:workflow · Returns 202 Accepted

curl -sS -X POST "https://copulse.app/api/v1/integrations/outbound/trigger" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "hubspot-deal-991-workflow",
    "workflow_id": "YOUR_WORKFLOW_UUID",
    "contact": {
      "name": "Priya Sharma",
      "external_id": "hs-contact-42",
      "phone": "+919876543210",
      "properties": { "first_name": "Priya" }
    },
    "properties": {
      "deal_name": "Skyline Towers Unit 4B",
      "deal_stage": "site_visit_booked"
    }
  }'

Response

{
  "success": true,
  "data": {
    "request_id": "uuid",
    "event_id": "uuid",
    "customer_id": "uuid",
    "workflow_id": "YOUR_WORKFLOW_UUID",
    "status": "accepted"
  },
  "error": null
}

CoPulse resolves the customer, merges properties into CRM account fields, stores an immutable trigger snapshot, then publishes workflow.requested with payload.properties and payload.customer_id.

Create or update a contact

POST /api/v1/integrations/crm/contacts

Scope crm:write · Returns 201 when created, 200 when matched

curl -sS -X POST "https://copulse.app/api/v1/integrations/crm/contacts" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Priya Sharma",
    "phone": "+919876543210",
    "email": "priya@example.com",
    "company": "Skyline Towers",
    "custom_properties": { "tier": "gold", "score": 12 },
    "custom_properties_merge_mode": "overwrite"
  }'

Upserts by phone and/or email within the API key's project. custom_properties are stored on the linked CRM account (not the person row). Require at least one of email or phone.

Response

{
  "success": true,
  "data": {
    "customer_id": "uuid",
    "created": true,
    "matched_by": null,
    "account_key": "co:skyline towers"
  },
  "error": null
}

Create or update an account

POST /api/v1/integrations/crm/accounts

Scope crm:write · Returns 201 when a primary contact is created

curl -sS -X POST "https://copulse.app/api/v1/integrations/crm/accounts" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Skyline Towers",
    "primary_contact": {
      "name": "Priya Sharma",
      "phone": "+919876543210"
    },
    "custom_properties": { "industry": "Real Estate", "region": "Pune" },
    "custom_properties_merge_mode": "fill_blank"
  }'

Prefer primary_contact (and optional company name) to create an account. To patch an existing account only, send account_key (e.g. co:skyline towers) with custom_properties. Set custom_properties_replace: true to replace the whole bag.

Create a deal

POST /api/v1/integrations/crm/deals

Scope crm:write · Returns 201 Created

curl -sS -X POST "https://copulse.app/api/v1/integrations/crm/deals" \
  -H "Authorization: Bearer cp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "CONTACT_UUID",
    "title": "Skyline Towers Unit 4B",
    "stage": "qualified",
    "value": 2500000,
    "currency": "INR",
    "custom_properties": { "unit": "4B", "tower": "A" }
  }'

customer_id must belong to the same project as the API key. Deal custom_properties are validated against active PropertyDefinitions for pipeline items when configured.

Poll request status

GET /api/v1/integrations/outbound/requests

Requires triggers:read or any outbound scope · Cursor pagination

curl -sS "https://copulse.app/api/v1/integrations/outbound/requests?limit=50&status=completed" \
  -H "Authorization: Bearer cp_live_YOUR_KEY"

Query params: status (optional), cursor (ISO timestamp from prior next_cursor), limit (1–100, default 50). Returns only requests created by the authenticated API key.

GET /api/v1/integrations/outbound/requests/:requestId

Requires triggers:read or any outbound scope

curl -sS "https://copulse.app/api/v1/integrations/outbound/requests/REQUEST_ID" \
  -H "Authorization: Bearer cp_live_YOUR_KEY"

Returns the integration trigger row for your org/project, including:

  • status: accepted, processing, completed, or failed
  • event_id, customer_id, conversation_id
  • properties: immutable snapshot from request time
  • resolved_preview: rendered text/template after {{…}} substitution
  • delivery: delivery hint from Postgres + events index

Delivery webhooks

Optionally configure a webhook_url when creating an API key in Setup → Integrations → Developer API. CoPulse POSTs a signed JSON payload as delivery progresses:

  • integration.request.sent / delivery_state: "sent" — provider accepted the send (WhatsApp/SMS/RCS; handset DLR still pending)
  • integration.request.completed / delivery_state: "delivered" — delivered (or read) on the device, or provider-accept for email/web
  • integration.request.failed / delivery_state: "failed" — rejected or undeliverable (wallet debit is refunded for template sends)

Headers on every delivery:

  • X-CoPulse-Timestamp — Unix timestamp used in the signature
  • X-CoPulse-Signaturesha256= HMAC of {timestamp}.{raw_body}

Example payload (handset delivered):

{
  "event": "integration.request.completed",
  "request_id": "uuid",
  "idempotency_key": "hubspot-deal-991-whatsapp",
  "status": "completed",
  "delivery_state": "delivered",
  "occurred_at": "2026-09-02T12:00:00.000Z"
}

Example payload (provider accepted, awaiting DLR):

{
  "event": "integration.request.sent",
  "request_id": "uuid",
  "idempotency_key": "hubspot-deal-991-whatsapp",
  "status": "processing",
  "delivery_state": "sent",
  "occurred_at": "2026-09-02T12:00:00.000Z"
}

Poll GET …/outbound/requests/:requestId if your endpoint is down. Webhook delivery is best-effort with a 10s timeout; retries are not automatic today.

Rate limits

Production enforces per-minute limits (returns 429 with Retry-After):

BucketLimit
Per API key120 requests / minute
Per project600 requests / minute

Failed responses use { success: false, data: null, error: { code, message } }.

Error codes

Many failures include a machine-readable code alongside error:

CodeHTTPMeaning
MISSING_PHONE400Phone required for WhatsApp, SMS, or RCS
MISSING_EMAIL400Email required for email channel
TEMPLATE_NOT_FOUND404Library template id not found
WALLET_NOT_FOUND402Organization wallet missing
INSUFFICIENT_BALANCE402Wallet balance too low for send/call
REQUEST_INCOMPLETE409Accept response still processing
CUSTOM_PROPERTIES_INVALID400CRM custom property validation failed
RATE_LIMITED429Per-key or per-project rate limit exceeded

Deprecated: low-level triggers

POST /api/v1/integrations/triggers and GET /api/v1/integrations/triggers/:triggerId remain for backward compatibility but return a Deprecation header. Prefer the typed outbound endpoints above and poll /outbound/requests/:requestId instead.

Idempotency

Every request requires idempotency_key (max 200 chars), unique per project. Retrying with the same key returns the original request_id and does not double-send.

  1. Generate a stable key from your source system (e.g. hubspot-{dealId}-{action}).
  2. On network timeout, retry the POST with the same body and key.
  3. Poll status until delivery.state is terminal.

Contact properties vs trigger snapshot

Send CRM fields in contact.properties. CoPulse merges them into the contact's CRM account record (live state used by campaigns and agents).

The same merged values are copied to IntegrationTrigger.properties at request time, an immutable snapshot for auditing. Later CRM edits do not change past trigger rows.

FieldWhere it livesMutable?
contact.properties in API requestInput only
CRM account custom_propertiesLive contact/accountYes (merge modes)
IntegrationTrigger.propertiesPer-request snapshotNo

property_merge_mode / custom_properties_merge_mode: fill_blank (default) or overwrite. Dedicated CRM create endpoints also accept custom_properties on contacts (account bag), accounts, and deals.

HubSpot HTTP action setup

Trigger CoPulse when a HubSpot workflow enrolls a contact (e.g. deal stage = Won):

  1. In HubSpot, open Automation → Workflows and create or edit a contact-based workflow.
  2. Add an enrollment trigger (e.g. Deal stage is any of Won).
  3. Add action Send a webhook (or Custom code / HTTP if your tier uses it).
  4. Method: POST. URL: https://copulse.app/api/v1/integrations/outbound/messages (or .../outbound/trigger for workflow-first automation).
  5. Headers: Authorization: Bearer cp_live_… and Content-Type: application/json.
  6. Map HubSpot tokens into to.phone, contact.name, and contact.properties.
  7. Set idempotency_key to a HubSpot unique value, e.g. {{ contact.id }}-won-whatsapp.
  8. Test with one contact. In CoPulse, open Setup → Integrations → Event Bus Activity to confirm delivery.