Sync Your CRM
Most teams use Kirim alongside an existing CRM (HubSpot, Salesforce, Pipedrive, Notion). This guide shows the patterns we’ve seen work in production for bidirectional sync.
The data model
Section titled “The data model”Map Kirim resources to CRM concepts:
| Kirim | CRM |
|---|---|
contact | Person / Lead / Contact |
conversation | Engagement / Ticket |
message | Note / Activity / Log entry |
label | Tag / Custom Field |
metadata on contact | Custom field bag |
Patterns by direction
Section titled “Patterns by direction”Kirim → CRM (one-way enrichment)
Section titled “Kirim → CRM (one-way enrichment)”When a new contact appears in Kirim, sync it to your CRM. Two mechanisms, pick one:
Option 1: Webhook-driven (real-time)
Section titled “Option 1: Webhook-driven (real-time)”Subscribe to contact.created and contact.updated. On each event,
look up or upsert by contact.phone_number in your CRM:
app.post('/kirim-webhook', async (req, res) => { // ... signature verification, dedup ... const event = JSON.parse(req.body.toString('utf8'))
switch (event.type) { case 'contact.created': await crm.upsertPerson({ phone: event.data.contact.phone_number, name: event.data.contact.name, source: 'kirim_' + event.data.acquisition_source, kirimContactId: event.data.contact.id, }) break
case 'contact.updated': // Only patch the fields Kirim says changed. const patch: Partial<CrmPerson> = {} for (const field of event.data.changed_fields) { if (field in event.data.contact) { patch[field] = event.data.contact[field] } } await crm.patchPersonByPhone(event.data.contact.phone_number, patch) break }
res.status(200).send('ok')})Pro: Real-time. Sub-second latency from contact creation to CRM update. Con: Requires reliable webhook endpoint + secret management.
Option 2: Periodic poll (batch)
Section titled “Option 2: Periodic poll (batch)”Run a cron job that lists Kirim contacts modified since the last sync:
async function syncContactsSince(since: Date) { let cursor: string | null = null do { const url = new URL('https://api-kckit.kirim.chat/v1/contacts') url.searchParams.set('limit', '100') if (cursor) url.searchParams.set('cursor', cursor)
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.KIRIM_KEY}` }, }) const body = await res.json()
// Filter client-side to rows updated since the watermark. // (Server-side `updated_after` is on the Phase 4 roadmap.) for (const c of body.data) { if (new Date(c.updated_at) > since) { await crm.upsertPerson({ phone: c.phone_number, name: c.name, email: c.email, kirimContactId: c.id, }) } }
cursor = body.next_cursor } while (cursor)}Pro: Resumable, no webhook surface area to maintain. Con: Latency = your cron interval. Burns read quota proportional to your contact count.
CRM → Kirim (one-way enrichment)
Section titled “CRM → Kirim (one-way enrichment)”When you enrich a person in your CRM (add notes, change name, tag them), push the changes back to Kirim:
async function pushPersonToKirim(person: CrmPerson) { // Find by phone — Kirim doesn't have a "find by external system id" // endpoint, so phone is your join key. const res = await fetch( `https://api-kckit.kirim.chat/v1/contacts?phone=${encodeURIComponent(person.phone)}`, { headers: { Authorization: `Bearer ${process.env.KIRIM_KEY}` } }, ) const { data } = await res.json()
if (data.length === 0) { // Create. await fetch('https://api-kckit.kirim.chat/v1/contacts', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.KIRIM_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ phone_number: person.phone, name: person.name, email: person.email, metadata: { crm_id: person.id, crm_segment: person.segment }, }), }) } else { // Update. const kirimId = data[0].id await fetch(`https://api-kckit.kirim.chat/v1/contacts/${kirimId}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${process.env.KIRIM_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: person.name, email: person.email, metadata: { crm_id: person.id, crm_segment: person.segment }, }), }) }}Bidirectional with conflict resolution
Section titled “Bidirectional with conflict resolution”If both sides can mutate, you need conflict resolution. The simplest rule that scales: last-writer-wins on a per-field basis, with the CRM as the source of truth for human-edited fields (name, email, custom tags) and Kirim as the source of truth for behavioural fields (last_message_at, acquisition_source).
Don’t try to sync metadata in both directions — pick one side to own each metadata key.
Bulk labelling
Section titled “Bulk labelling”When a CRM segment changes (e.g. “all customers who haven’t bought in 30 days”), bulk-apply a Kirim label:
const staleCustomerIds = await crm.queryStaleCustomers() // returns Kirim contact ids
// Cap is 1000 per request — paginate.const chunks = chunk(staleCustomerIds, 1000)for (const ids of chunks) { await fetch('https://api-kckit.kirim.chat/v1/contacts/bulk_label', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.KIRIM_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ contact_ids: ids, label_id: 'lbl_…', operation: 'attach', }), })}The response counts how many were applied, skipped because they’re cross-org, or skipped because of team mismatch:
{ "data": { "applied": 847, "skipped_cross_org": 12, "skipped_team_mismatch": 0 }}Conversation handoff
Section titled “Conversation handoff”When a Kirim conversation needs human attention (e.g. customer asked for a refund), use the assignee + label vocabulary to route it:
// 1. Tag the conversation so your inbox filter picks it up.await fetch(`https://api-kckit.kirim.chat/v1/conversations/${cnvId}/labels`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.KIRIM_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ label_id: 'lbl_refund_request' }),})
// 2. Assign it to the right agent based on availability / round-robin.const agent = await pickAvailableAgent('refunds')await fetch(`https://api-kckit.kirim.chat/v1/conversations/${cnvId}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${process.env.KIRIM_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ assigned_to: agent.userId }),})
// 3. Open Kirim Inbox for the agent in your CRM workflow.notifyAgent(agent, `https://app.kirim.dev/inbox/${cnvId}`)Subscribe to conversation.assigned to react to assignments made
inside Kirim too — full bidirectional visibility.
What’s next
Section titled “What’s next”- API Reference → Contacts — full contact endpoint catalogue.
- Subscribe to Webhooks — set up the real-time side of the sync.