API Design for CRM: Leads, Contacts, Deals, Quotes

Sep 14, 2023 5 min read

Introduction

On the outside, a CRM seems to just be a few data tables: leads, contacts, deals, quotes. But when designing the real API, problems start appearing:

How is a lead different from a contact?
When does a lead become a deal?
Does a quote belong to a deal or a customer?
Where is follow-up activity stored?
Should the API be named after the action or the resource?

This article documents how I design an API for a small CRM, in a practical and easily extensible way. I rely on REST API design principles like resource-oriented design, using nouns for resource names, HTTP methods for actions, and adding must-haves like pagination, filtering, and idempotency for critical operations.

1. Design Principles

Before writing endpoints, I want to stick to a few principles:

1. Resource first, action second

Instead of:

POST /create-lead
POST /update-deal-status

Use:

POST /leads
PATCH /deals/{dealId}

The HTTP method already describes the action. The URL should describe the resource.

2. API must reflect the real workflow

A CRM is not just CRUD. It’s a flow:

Lead → Contact/Customer → Deal → Quote → Follow-up → Close

If the API doesn’t reflect this flow, building automation later will be very hard.

3. Avoid over-engineering

The first version doesn’t need microservices. A clear backend module with a good database schema is enough.

2. Core resources

I’ll start with 6 resources:

/leads
/contacts
/customers
/deals
/quotes
/activities

Brief explanation:

  • Lead: a person or company capable of buying, but not clear enough to be a customer.
  • Contact: a specific contact person.
  • Customer: an organization/individual confirmed as a customer or account.
  • Deal: a sales opportunity.
  • Quote: a quotation linked to a deal or customer.
  • Activity: history of calls, emails, messages, notes, follow-ups.

3. APIs for Leads

GET    /leads
POST   /leads
GET    /leads/{leadId}
PATCH  /leads/{leadId}
DELETE /leads/{leadId}

Example for creating a lead:

POST /leads
{
  "name": "Nguyen Van A",
  "email": "a@example.com",
  "phone": "+84900000000",
  "source": "website_form",
  "companyName": "ABC Manufacturing",
  "note": "Interested in CRM automation"
}

Response:

{
  "id": "lead_123",
  "name": "Nguyen Van A",
  "status": "new",
  "source": "website_form",
  "createdAt": "2026-06-24T00:00:00Z"
}

Lead status could be:

new
contacted
qualified
unqualified
converted

4. Converting a lead to customer/deal

This is a special operation. We can use an action endpoint because it represents a workflow, not just a field update.

POST /leads/{leadId}:convert

Payload:

{
  "createCustomer": true,
  "createDeal": true,
  "dealTitle": "CRM automation package",
  "estimatedValue": 2500
}

Result:

{
  "leadId": "lead_123",
  "customerId": "cus_456",
  "contactId": "con_789",
  "dealId": "deal_001"
}

For this type of operation, you should think about an idempotency key. If the request times out and the client retries, the system shouldn’t create 2 duplicate customers or deals.

5. APIs for Contacts and Customers

GET    /contacts
POST   /contacts
GET    /contacts/{contactId}
PATCH  /contacts/{contactId}

GET    /customers
POST   /customers
GET    /customers/{customerId}
PATCH  /customers/{customerId}

If the customer is a company, the contact is a person in the company:

GET  /customers/{customerId}/contacts
POST /customers/{customerId}/contacts

Example customer:

{
  "id": "cus_456",
  "name": "ABC Manufacturing",
  "type": "company",
  "industry": "manufacturing",
  "ownerUserId": "user_001"
}

6. APIs for Deals

The deal is where the sales pipeline happens.

GET    /deals
POST   /deals
GET    /deals/{dealId}
PATCH  /deals/{dealId}
DELETE /deals/{dealId}

Deal stages:

new
qualified
proposal
negotiation
won
lost

Example:

POST /deals
{
  "customerId": "cus_456",
  "title": "CRM automation package",
  "stage": "qualified",
  "estimatedValue": 2500,
  "currency": "USD",
  "expectedCloseDate": "2026-07-15"
}

To update stage:

PATCH /deals/deal_001
{
  "stage": "proposal"
}

You should save stage changes to activities or deal_stage_history, because the sales pipeline needs an audit trail.

7. APIs for Quotes

A quote is usually linked to a deal.

GET    /deals/{dealId}/quotes
POST   /deals/{dealId}/quotes
GET    /quotes/{quoteId}
PATCH  /quotes/{quoteId}
POST   /quotes/{quoteId}:send
POST   /quotes/{quoteId}:accept
POST   /quotes/{quoteId}:reject

Example creating quote:

POST /deals/deal_001/quotes
{
  "items": [
    {
      "name": "CRM setup",
      "quantity": 1,
      "unitPrice": 1500
    },
    {
      "name": "Workflow automation package",
      "quantity": 1,
      "unitPrice": 1000
    }
  ],
  "currency": "USD",
  "validUntil": "2026-07-30"
}

Quote status:

draft
sent
accepted
rejected
expired

With send, accept, reject, I use action endpoints because these are clear domain actions, not just field edits.

8. APIs for Activities

Activities give the CRM a working history.

GET  /activities?entityType=deal&entityId=deal_001
POST /activities

Payload:

{
  "entityType": "deal",
  "entityId": "deal_001",
  "type": "follow_up",
  "content": "Send quote tomorrow morning",
  "dueAt": "2026-06-25T02:00:00Z"
}

Entity types could be:

lead
contact
customer
deal
quote

This helps one activity table be used for many object types.

9. Pagination, filtering, sorting

The CRM list will grow. You shouldn’t return all records.

Example:

GET /leads?status=qualified&source=website_form&limit=20&cursor=abc
GET /deals?stage=proposal&sort=-createdAt

Response:

{
  "data": [],
  "nextCursor": "next_abc"
}

For a CRM, cursor pagination is usually better than offset if data changes constantly.

10. Error format

Should have a consistent error format:

{
  "error": {
    "code": "LEAD_NOT_FOUND",
    "message": "Lead not found",
    "requestId": "req_123"
  }
}

requestId is very useful when debugging production logs.

11. Conclusion

API design for CRM is not just CRUD. The important thing is to understand the real workflow:

lead capture
qualification
conversion
deal tracking
quote management
follow-up history

My main lessons:

  • The URL should revolve around resources, not actions.
  • Workflow actions like convert/send/accept can be custom endpoints.
  • CRM needs an activity log early on.
  • Pagination/filtering should be present from the start.
  • Important data creation operations should consider idempotency.

If done correctly, the API will not only serve the frontend, but also automation workflows and AI agents later.

References

New posts, shipping stories, and nerdy links straight to your inbox.

2x per month, pure signal, zero fluff.


Share this post on: