Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.propal.io/llms.txt

Use this file to discover all available pages before exploring further.

This guide shows how to build a bi-directional sync between Propal leads and your external contact database.

Overview

A typical contact sync involves:
  1. Initial import — Pull all contacts from your external system and create them as leads in Propal
  2. Ongoing sync — Periodically check for new or updated contacts and sync them
  3. Reverse sync — Push new Propal leads back to your external system

Initial import

Fetch all contacts from your external system and create them in Propal:
async function importContacts(client, contacts) {
  const results = { created: 0, skipped: 0, errors: [] };

  for (const contact of contacts) {
    try {
      // Check if lead already exists
      const { data: existing } = await client
        .request(`/leads?search=${encodeURIComponent(contact.email)}&limit=1`)
        .then(r => r.json());

      if (existing.length > 0) {
        results.skipped++;
        continue;
      }

      // Create the lead
      await client.request('/leads', {
        method: 'POST',
        body: JSON.stringify({
          name: contact.name,
          email: contact.email,
          company: contact.company,
          phone: contact.phone,
          address: contact.address,
          city: contact.city,
          country: contact.country,
        }),
      });

      results.created++;
    } catch (error) {
      results.errors.push({ contact: contact.email, error: error.message });
    }
  }

  return results;
}
For large imports (1000+ contacts), use limit=100 per page and respect the rate limits. Consider running the import during off-peak hours.

Ongoing sync

Set up a periodic job (cron, scheduled task) to sync changes:
async function syncContacts(client, externalContacts) {
  // 1. Fetch all Propal leads
  const propalLeads = await fetchAllLeads(client);

  // 2. Index by email for fast lookup
  const leadsByEmail = new Map(
    propalLeads
      .filter(l => l.email)
      .map(l => [l.email.toLowerCase(), l])
  );

  for (const contact of externalContacts) {
    const existing = leadsByEmail.get(contact.email.toLowerCase());

    if (existing) {
      // Update if data has changed
      if (hasChanges(existing, contact)) {
        await client.request(`/leads/${existing.id}`, {
          method: 'PATCH',
          body: JSON.stringify({
            name: contact.name,
            company: contact.company,
            phone: contact.phone,
          }),
        });
      }
    } else {
      // Create new lead
      await client.request('/leads', {
        method: 'POST',
        body: JSON.stringify({
          name: contact.name,
          email: contact.email,
          company: contact.company,
        }),
      });
    }
  }
}

function hasChanges(lead, contact) {
  return (
    lead.name !== contact.name ||
    lead.company !== contact.company ||
    lead.phone !== contact.phone
  );
}

Reverse sync — push Propal leads to your CRM

Fetch new leads from Propal and push them to your external system:
async function reverseSync(client, lastSyncTimestamp) {
  // Fetch leads created after the last sync
  const leads = await fetchAllLeads(client);

  const newLeads = leads.filter(
    lead => new Date(lead.created_at) > new Date(lastSyncTimestamp)
  );

  for (const lead of newLeads) {
    await pushToExternalCRM({
      name: lead.name,
      email: lead.email,
      company: lead.company,
      phone: lead.phone,
      source: 'Propal',
    });
  }

  return newLeads.length;
}

Enriching leads with proposal data

For each lead, you can also fetch their associated proposals:
async function getLeadWithProposals(client, leadId) {
  const [leadResponse, proposalsResponse] = await Promise.all([
    client.request(`/leads/${leadId}`).then(r => r.json()),
    client.request(`/leads/${leadId}/proposals`).then(r => r.json()),
  ]);

  return {
    ...leadResponse,
    proposals: proposalsResponse.data,
    totalProposals: proposalsResponse.data.length,
    hasAcceptedProposal: proposalsResponse.data.some(
      p => p.deal_status === 'approved'
    ),
  };
}

Best practices

  • Deduplicate by email. Use the search parameter to check for existing leads before creating duplicates.
  • Handle rate limits. Add delays between requests during bulk operations. See Rate Limiting.
  • Log sync results. Track created, updated, and skipped records for debugging.
  • Use incremental sync. Only process records that changed since the last sync, not the entire dataset.