How to Sync Pipedrive Deals with Your App Using the REST API

October 23, 2026

A CRM sync that works on 200 deals falls over at 50,000. The difference is pagination strategy, webhook handling, and understanding that Pipedrive’s rate limits are token-based rather than call-based.

Direct answer: to sync Pipedrive deals into your app, register an OAuth app, crawl /api/v2/deals using cursor pagination rather than start, subscribe to the updated.deal webhook for incremental changes, and track your token budget from the x-ratelimit-remaining response header. The API v2 allows roughly 80 requests per 2 seconds per token.

How do you authenticate?

Two paths. A personal API token is fine for a single-company internal tool; OAuth is required for anything you sell.

OAuth flow, in short: redirect to https://oauth.pipedrive.com/oauth/authorize?client_id=...&redirect_uri=..., receive a code, exchange it at /oauth/token for an access token (1 hour) and a refresh token. Store the refresh token; refresh before expiry rather than after a 401.

Personal tokens: Settings → Personal preferences → API, and the token appears as ?api_token= or an x-api-token header. Use the header. Query-string tokens end up in server logs.

Do not reuse one token across customers. Access is scoped to the account that created it, and a token embedded in your source is a data breach with a delay.

How does cursor pagination work?

The v2 API returns {data: [...], additional_data: {next_cursor: "..."}}. Loop until next_cursor is null.

cursor = None
while True:
    params = {"limit": 100, "sort_by": "update_time", "sort_direction": "asc"}
    if cursor:
        params["cursor"] = cursor
    r = session.get(f"{BASE}/api/v2/deals", params=params)
    r.raise_for_status()
    j = r.json()
    yield from j["data"]
    cursor = j["additional_data"]["next_cursor"]
    if not cursor:
        break

limit maxes at 100 for deals. Sorting by update_time ascending is what makes incremental sync possible — store the last seen timestamp, and next run start from there.

Do not use the old v1 start offset parameter. It gets slower as offsets grow because the database still walks the skipped rows, and Pipedrive has deprecated it for most collections.

Why is rate limiting harder than it looks?

Pipedrive uses a token bucket. Each token has a budget that refills, and reads cost less than writes. The response headers x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset tell you where you stand.

Read the headers rather than hardcoding a sleep. A blind sleep(0.2) between calls either wastes time or still gets 429s depending on how many writes you are mixing in.

On a 429, back off exponentially from one second with jitter, up to about 30 seconds. Pipedrive’s retry semantics are clean — resending a GET is always safe, and the Idempotency-Key header makes POSTs safe too.

How do webhooks fit in?

The webhook gives you near-real-time updates without polling. Register with:

POST /api/v2/webhooks
{
  "subscription_url": "https://yourapp.com/hooks/pipedrive",
  "event_action": "updated",
  "event_object": "deal"
}

Events arrive as a batch: {data: [{action, object, change, meta}]}. Verify the meta signature using your basic-auth credentials — Pipedrive signs with HMAC-SHA256 over the raw body.

Critical detail: the webhook payload tells you a deal changed, not what it now contains. Fetch the deal by ID before applying the change, or you will reconstruct state from partial diffs and lose data when updates arrive out of order.

Also handle duplicates. Delivery is at-least-once, and a retried event looks identical to a first delivery. Store event_id values and skip repeats.

What sync strategy should you use?

Full crawl for the first load, incremental thereafter:

  1. Initial: page through all deals sorted by update_time, chunking 5,000 per run to stay polite.
  2. Ongoing: webhook for changes plus a nightly incremental crawl from the last watermark as a safety net.
  3. Reconciliation: a weekly full comparison of IDs where the local count differs from GET /api/v2/deals/summary.

Belt and braces, but webhooks drop. A nightly incremental catches what the webhook missed, and it costs a few hundred requests.

Where the API limits bite

History data is a different API — /api/v1/deals/{id}/flow — and it is expensive. Pulling full activity history for 50,000 deals is not feasible in a sync; fetch it lazily when a user opens a deal.

Custom fields arrive as long hash keys, not human names. Cache the field definitions from /api/v2/dealFields and map them once.

And deleted records need the /api/v1/deals/deleted endpoint separately. A webhook fires for deletion, but if you missed it, nothing in the regular crawl tells you the deal is gone.

Key Takeaways

  • Use API v2 with cursor pagination and sort_by=update_time — v1 start offsets degrade at scale.
  • Token-bucket limits mean reads and writes cost differently; read x-ratelimit-remaining instead of sleeping blindly.
  • Webhook payloads signal a change, not the new state — always refetch the record by ID.
  • Combine webhooks with a nightly incremental crawl; webhook delivery is at-least-once and occasionally drops.
  • History and custom field names need separate endpoints; fetch history lazily or the crawl becomes impractical.