When CRM and ERP operate in silos, your sales team is flying blind. A sales rep who creates a quote in the CRM needs to check product availability, pricing, and delivery times stored in the ERP — without integration, they call the warehouse, email finance, and wait. This slowdown costs deals. NetSuite research shows that ERP-CRM integration shortens sales cycles, improves targeting, and increases profits. For Indonesian businesses where relationship selling is paramount and delays signal disorganization, real-time CRM-ERP connectivity is a competitive advantage. At Commsult, I've integrated our custom ERP with both custom-built CRM features and third-party CRMs. Here's what I've learned.
Not everything should sync — over-integration creates noise and maintenance burden. The highest-value sync points are: Customer Master (single source of truth — decide if ERP or CRM owns it and sync one direction), Product and Pricing (CRM pulls current pricing and availability from ERP, never maintains its own price list), Quotations (CRM creates quotes, winning quotes convert to Sales Orders in ERP automatically), Order Status (ERP order status and shipment tracking visible in CRM so sales reps can answer customer questions), and Payment Status (outstanding invoices and credit limit visible in CRM to help sales prioritize collections follow-up).
The customer master sync question — who owns the record? — is the most contentious integration decision. Our approach: CRM owns new customer creation (sales team creates prospects and customers in CRM), and the ERP consumes customers (the CRM pushes newly created customers to ERP via webhook when a deal is won). The ERP adds financial attributes (credit limit, payment terms, AR balance) that flow back to CRM via a daily sync job. This one-directional real-time sync (CRM to ERP) plus one-directional daily sync (ERP to CRM) avoids complex bidirectional conflict resolution.
CRM ↔ ERP Integration Architecture
CRM (HubSpot / custom) ERP (NestJS + PostgreSQL)
───────────────────── ──────────────────────────
Contacts & Companies ──→ customers (master)
(CRM owns new records) (ERP adds: credit_limit,
payment_terms, ar_balance)
Deals / Pipelines ──→ sales_orders (on deal WON)
(real-time webhook) auto-created with line items
←── Product Catalog products (ERP owns)
←── Inventory Available stock_movements (computed)
←── Invoice Status ar_invoices (daily sync)
←── AR Balance ar_summary (daily sync)
Integration Flow:
CRM Deal → WON
│ (HubSpot webhook: deal.stageId = closedwon)
▼
NestJS CrmIntegrationModule
│ validate signature
│ fetch deal details from HubSpot API
│ create Sales Order in ERP
│ post order confirmation back to HubSpot (note)
▼
ERP Sales Order created
│ → warehouse receives pick task
│ → shipment status updates
└── status webhook back to CRM (daily sync)
CRM Gateway API (read-only for CRM):
GET /crm/customers/:id/summary
GET /crm/products/availability?skus[]=...
GET /crm/invoices?customerId=...From my experience building ERP systems at Commsult: expose ERP data to the CRM via a read-only API layer, not by giving CRM users direct ERP access. This keeps the data separation clean, allows you to version and deprecate the integration API independently of ERP changes, and prevents CRM users from accidentally triggering ERP workflows. We built a CRM Gateway API in NestJS that is specifically designed for CRM consumption — it returns simplified, flattened data structures that CRM systems expect, not the normalized relational structures the ERP uses internally.
The quote-to-order transition is the highest-value integration point. When a sales rep marks a CRM deal as 'Won' and attaches the final quotation, the integration: creates a Sales Order in the ERP with the exact line items from the CRM quote, assigns it to the correct order processing queue, generates a Sales Order confirmation document (PDF), and sends it to the customer and internal sales. This automation eliminates the manual re-entry of deal details from CRM into ERP — a 15-30 minute error-prone task that the sales admin team used to do for every won deal.
One of the most valuable CRM-ERP integrations is surfacing real-time inventory availability to the sales team. When a sales rep is building a quote, they need to know: is this product in stock? When can it be delivered? Our CRM Gateway API exposes an /availability endpoint that takes a list of product SKUs and quantities and returns: in-stock quantity, available-to-promise quantity (accounting for existing committed orders), and expected next receipt date (from open POs). Sales reps get this information while building the quote, enabling them to commit accurate delivery dates to customers.
// NestJS: CRM Gateway API — availability endpoint
@Get('/crm/products/availability')
@UseGuards(CrmApiKeyGuard)
async getProductAvailability(
@Query('skus') skus: string[],
@Query('quantities') quantities: number[],
) {
const results = await Promise.all(
skus.map(async (sku, i) => {
const item = await this.itemRepo.findBySku(sku);
if (!item) return { sku, available: false, inStock: 0 };
// Current stock (materialized view)
const inStock = await this.inventoryService.getCurrentStock(item.id);
// Available-to-promise (stock minus committed orders)
const committed = await this.salesOrderService.getCommittedQty(item.id);
const atp = inStock - committed;
// Next receipt date if ATP insufficient
const nextReceipt = atp < (quantities[i] ?? 1)
? await this.poService.getNextExpectedReceipt(item.id)
: null;
return { sku, inStock, committed, atp, nextReceipt };
})
);
return results;
}
// Quote-to-Order: HubSpot deal WON webhook handler
@Post('/webhooks/hubspot/deal-won')
async handleDealWon(@Body() body: HubSpotDealWonPayload) {
// Validate HubSpot signature
const sig = crypto.createHmac('sha256', process.env.HUBSPOT_WEBHOOK_SECRET)
.update(JSON.stringify(body)).digest('hex');
if (sig !== body.signature) throw new UnauthorizedException();
// Fetch full deal from HubSpot API (webhook body is minimal)
const deal = await this.hubspotClient.getDeal(body.objectId);
const lineItems = await this.hubspotClient.getDealLineItems(body.objectId);
// Map CRM line items to ERP items
const orderLines = await Promise.all(
lineItems.map(async (li) => ({
itemId: (await this.itemRepo.findBySku(li.properties.hs_sku)).id,
quantity: parseInt(li.properties.quantity),
unitPrice: parseFloat(li.properties.price),
}))
);
// Create Sales Order in ERP
const order = await this.salesOrderService.create({
customerId: deal.properties.customer_erp_id,
crmDealId: body.objectId,
lines: orderLines,
deliveryDate: deal.properties.closedate,
});
// Post confirmation note back to HubSpot
await this.hubspotClient.createNote(body.objectId,
`Sales Order ${order.orderNumber} created in ERP`
);
return { orderId: order.id };
}Our ERP has a built-in lightweight CRM module (contacts, deals, pipelines) for clients who don't want a separate CRM. For clients with existing CRMs (HubSpot being the most common in our client base), we build the integration via a dedicated CrmIntegrationModule in NestJS that exposes webhooks for inbound events and an API client for outbound sync. HubSpot's native webhook system triggers our ERP's endpoint when a deal stage changes. Our endpoint validates the webhook signature, processes the event, and triggers the appropriate ERP workflow.
Customer data drift between CRM and ERP is the most common integration failure mode. Sales reps update customer addresses in the CRM; finance updates them in the ERP; six months later the two records have diverged and invoices go to wrong addresses. Define a clear data ownership policy: one system is the master for each data field, and changes flow in one direction only. We enforce this by making ERP customer fields read-only in the CRM UI (displayed but not editable) and vice versa for CRM-owned fields. Bidirectional sync for the same field is a maintenance nightmare.
With CRM and ERP integrated, you can build sales performance reports that span both systems: lead-to-order conversion rate (CRM leads that became ERP orders), average deal cycle time (CRM deal created date to ERP order date), win rate by product and customer segment (CRM deal outcomes vs. ERP product mix), sales rep performance (revenue from won deals by rep, using CRM deal owner linked to ERP order), and customer lifetime value (total AR invoices from ERP grouped by CRM customer acquisition channel). These cross-system insights are impossible without integration.
In Indonesian B2B sales, relationship and trust are paramount. Sales calls often start with a review of the customer's history: 'You bought X last quarter at Y price, would you like to reorder?' This context requires ERP data in the CRM. We surface the last 5 orders, average order value, current outstanding balance, and days since last order on the CRM customer page. Sales reps find this data invaluable — they walk into meetings knowing the full customer picture. One client reported that their sales reps were closing 25% more reorder opportunities because they were proactively following up on customers whose order frequency had dropped, a pattern visible only through the ERP-CRM integrated view.