[object Object]

Per-App APIs

Each Freshworks app exposes its own REST API on a product-specific subdomain: https://{domain}.freshdesk.com/api/v2/, https://{domain}.freshservice.com/api/v2/, https://{domain}.myfreshworks.com/crm/sales/api/ for Freshsales, and https://api.freshchat.com/v2/ for Freshchat. Patterns are similar (REST, JSON, pagination) but distinct enough that a wrapper that abstracts all four ends up leaky. Build per-product clients, share only the auth and retry layers.

Authentication

Freshdesk and Freshservice still use API key auth via HTTP Basic, sending the key as username and any string (X is conventional) as password. Freshsales uses an Authorization: Token token=... header. Freshchat uses a JWT bearer token. OAuth 2.0 is available for Marketplace apps and is the right choice when distributing a multi-tenant integration. Rotate keys quarterly and store them in a secret manager — never commit them, and never log the request headers.

const auth = 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64');
const res = await fetch(`https://${domain}.freshdesk.com/api/v2/tickets/${id}`, {
  headers: { Authorization: auth, 'Content-Type': 'application/json' }
});

Common Operations

CRUD on tickets, contacts, deals, problems, and changes follows predictable verbs (GET /tickets, POST /tickets, PUT /tickets/{id}, DELETE /tickets/{id}). Search uses a separate query syntax — Freshdesk’s filter API takes Lucene-style queries ("priority:3 AND status:2") URL-encoded into a query parameter. Bulk endpoints exist for ticket and contact updates but cap at 100 records per request. Pagination is page-based (page=2&per_page=100) with a hard ceiling of 300 pages on most endpoints — past that, you must use updated_since time-windowed queries.

Rate Limits

Limits are per minute and per plan. Freshdesk Garden plan caps at 200 calls/minute, Estate at 400, Forest at 700; Freshservice tiers parallel that. Exceeding returns HTTP 429 with a Retry-After header (seconds to wait). Honor it with exponential backoff and jitter:

async function callWithRetry(fn, max = 5) {
  for (let i = 0; i < max; i++) {
    const res = await fn();
    if (res.status !== 429) return res;
    const wait = parseInt(res.headers.get('retry-after') || '60', 10);
    await new Promise(r => setTimeout(r, (wait + Math.random()) * 1000));
  }
  throw new Error('rate-limited after retries');
}

Watch the X-RateLimit-Remaining and X-RateLimit-Total headers — when remaining drops below 10% of total, slow your batch jobs proactively rather than waiting for the 429.

Webhooks

Most apps support webhooks fired on entity events (ticket_create, ticket_update, contact_create, deal_won). Subscribe per event type from the admin UI or programmatically. Webhook payloads include the entity snapshot but not the full diff — if you need before/after, you must keep state on your side. Verify the signature header (X-Freshworks-Signature HMAC-SHA256 of body using the shared secret) before processing. Reply with HTTP 200 within 5 seconds or the platform retries with exponential backoff up to 4 times, then drops the event.

Common Failure Modes

Three to avoid. First, polling instead of webhooks — polling burns rate limit and lags real-time by minutes. Second, ignoring partial-failure semantics on bulk endpoints; the response includes per-record status and a 200 overall does not mean every record succeeded. Third, using a personal user’s API key in production — the key dies when that user offboards. Create a dedicated service account user, assign it minimum-required role permissions, and use its key.

What to do this week

Pick one polling integration in your stack and replace it with a webhook. Measure the call-volume reduction; it is usually 90%+ and frees rate-limit budget for the integrations you actually need to poll.

[object Object]
Share