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
| Scope | Allows |
|---|---|
outbound:message | POST outbound messages (WhatsApp, SMS, RCS, email, web) |
outbound:call | POST outbound phone calls |
outbound:workflow | POST workflow kickoffs (automation-first) |
crm:write | POST contacts, accounts, and deals (with custom properties) |
triggers:read | Poll request status (also allowed with outbound scopes) |
Send an outbound message
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" } }
}'
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
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
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
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
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
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
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.
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, orfailedevent_id,customer_id,conversation_idproperties: immutable snapshot from request timeresolved_preview: rendered text/template after{{…}}substitutiondelivery: 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/webintegration.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 signatureX-CoPulse-Signature—sha256=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):
| Bucket | Limit |
|---|---|
| Per API key | 120 requests / minute |
| Per project | 600 requests / minute |
Failed responses use { success: false, data: null, error: { code, message } }.
Error codes
Many failures include a machine-readable code alongside error:
| Code | HTTP | Meaning |
|---|---|---|
MISSING_PHONE | 400 | Phone required for WhatsApp, SMS, or RCS |
MISSING_EMAIL | 400 | Email required for email channel |
TEMPLATE_NOT_FOUND | 404 | Library template id not found |
WALLET_NOT_FOUND | 402 | Organization wallet missing |
INSUFFICIENT_BALANCE | 402 | Wallet balance too low for send/call |
REQUEST_INCOMPLETE | 409 | Accept response still processing |
CUSTOM_PROPERTIES_INVALID | 400 | CRM custom property validation failed |
RATE_LIMITED | 429 | Per-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.
- Generate a stable key from your source system (e.g.
hubspot-{dealId}-{action}). - On network timeout, retry the POST with the same body and key.
- Poll status until
delivery.stateis 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.
| Field | Where it lives | Mutable? |
|---|---|---|
contact.properties in API request | Input only | — |
CRM account custom_properties | Live contact/account | Yes (merge modes) |
IntegrationTrigger.properties | Per-request snapshot | No |
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):
- In HubSpot, open Automation → Workflows and create or edit a contact-based workflow.
- Add an enrollment trigger (e.g. Deal stage is any of Won).
- Add action Send a webhook (or Custom code / HTTP if your tier uses it).
- Method: POST. URL:
https://copulse.app/api/v1/integrations/outbound/messages(or.../outbound/triggerfor workflow-first automation). - Headers:
Authorization: Bearer cp_live_…andContent-Type: application/json. - Map HubSpot tokens into
to.phone,contact.name, andcontact.properties. - Set
idempotency_keyto a HubSpot unique value, e.g.{{ contact.id }}-won-whatsapp. - Test with one contact. In CoPulse, open Setup → Integrations → Event Bus Activity to confirm delivery.