On this page
Guides / Link to the conversation from your CRM

Link to the conversation from your CRM

Store, on each customer record in your system, a link that opens their conversation in Meteor, matched by phone number or tax ID.

Updated View .md
StackNode · Express · TypeScript SDK
Endpointscontacts.list · contacts.retrieve · webhooks.subscriptions.create · webhooks.constructEvent

Your team lives in your CRM, but customer conversations happen in Meteor. Every contact comes with an app_url: the link that opens their conversation in Meteor. Store it on the customer record in your system and whoever is looking at that record reaches the chat in one click.

The link is stable: it doesn't include the workspace name, so it keeps working if the workspace is renamed. Opening it requires a Meteor session with access to that contact. It is not a public link to the conversation.

{
  "id": 4821,
  "data": { "$name": "Ada Lovelace", "$phone": "+573001112233", "nit": "900123456" },
  "app_url": "https://met.meteor.com.co/abrir/137/contacto/4821"
}

There are two ways to fill it in. You can ask Meteor whenever you need it (step 2), or let Meteor notify you every time a contact is created, changes or writes in (step 3). If your system can receive a POST, the second keeps the record current without any polling.

Before you start

Your key needs contacts:read. To receive events, also webhooks:manage and conversations:read.

1. Pick the value you match on

Your system does the matching, with a value both sides have:

Find the contact with a filter and read its app_url:

import Met from '@meteor.ia/sdk';

const met = new Met({ apiKey: process.env.MET_API_KEY! });

async function meteorLink(taxId: string): Promise<string | null> {
  const page = await met.contacts.list({
    filters: JSON.stringify([{ field: 'nit', op: 'eq', value: taxId }]),
    limit: 2,
  });
  if (page.data.length !== 1) return null;   // 0: not in Meteor · 2+: check for duplicates
  return page.data[0].app_url ?? null;
}

A malformed condition is ignored, it doesn't error: if you misspell the field name, the filter returns every contact. That's why the example asks for limit: 2 and only accepts exactly one result.

If you already have the contact's id, met.contacts.retrieve(id) also returns the app_url.

3. Let Meteor send it

Subscribe to contact events. Every event about a contact carries its app_url in data: contact.created, contact.updated, contact.message.received, contact.message.sent and conversation.handoff.

const sub = await met.webhooks.subscriptions.create({
  url: 'https://your-crm.com/webhooks/met',
  enabled_events: ['contact.created', 'contact.updated', 'contact.message.received'],
  description: 'Conversation link on the customer record',
});

console.log(sub.secret);   // whsec_… — shown ONCE

The endpoint verifies the signature, answers fast and stores the link on your record:

app.post('/webhooks/met', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = met.webhooks.constructEvent(
      req.body,
      req.headers['x-met-signature'] as string,
      process.env.MET_WEBHOOK_SECRET!,
    );
  } catch {
    return res.status(400).send('invalid signature');
  }
  res.sendStatus(200);

  const { contact_id, app_url } = event.data as { contact_id: number; app_url?: string };
  if (!app_url) return;
  const contact = await met.contacts.retrieve(contact_id);
  await yourCrm.saveLink({
    phone: contact.data.$phone,
    taxId: contact.data.nit,
    link: app_url,
  });
});

Saving the same link twice does no harm, so this endpoint survives retries without keeping track of which events it already processed. The recipe Receive signed events in your backend covers signatures and retries in detail.

No code: from a flow

If you don't have a backend, a Meteor flow can send the link to your system with an HTTP request node. The variable is $contact.app_url, just like $contact.phone or $contact.name.