Read your CRM from your own tools
Give n8n, a dashboard, a warehouse or your own app permission to read leads, enquiries and follow-ups, and to follow every change through an append-only feed. The API is read-only: no key can change or delete anything in the CRM.
Base URL https://api.crm.diginetacademy.in/api/v1. Every endpoint is in the API reference.
Quick start
- An owner or admin opens Integrations → Send data out in Zinvee CRM and creates a key, ticking only what the tool needs. The key is shown once: copy it straight into the tool.
- Check it works.
whoamineeds no scope, returns no lead data and costs nothing, so make it the first step of every workflow:export CRM_KEY="tcrm_…" curl https://api.crm.diginetacademy.in/api/v1/public/whoami \ -H "X-API-Key: $CRM_KEY" - Read what you need, for example the enquiries still open in one campaign:
curl "https://api.crm.diginetacademy.in/api/v1/public/inquiries?lifecycle=open&campaignCode=<code>&limit=200" \ -H "X-API-Key: $CRM_KEY"
The whole API is published as an OpenAPI 3.1 file that Postman and an n8n HTTP node can import: /public/openapi.json (no key needed).
Keys and scopes
Send the key as X-API-Key: tcrm_…, or as Authorization: ApiKey tcrm_… for clients that can’t set their own header. The scheme word is ApiKey, not Bearer.
- A key reads only its own organization’s data, and only the parts that were ticked.
- It is shown once and stored only as a SHA-256 hash, so nobody can read it back. Lost it? Rotate gives the same key a new secret, and the old one stops at once.
- Revoke stops a leaked key on its very next request. Keys expire after 365 days unless another expiry is set when the key is made.
- Each key has its own limits, set when it is made and changeable later (see Rate limits).
| Scope | In the app | What it reads |
|---|---|---|
LEADS_READ | Read leads | Name, phone, status, owner and tags of every lead. |
INQUIRIES_READ | Read enquiries | Each enquiry’s campaign, stage, fee and balance. |
FOLLOWUPS_READ | Read follow-ups | Which calls are due, overdue or scheduled ahead. |
REFERENCE_READ | Read reference lists | Your stages, campaigns, sources, courses and staff. |
EVENTS_READ | Read the change feed | What changed, so a tool keeps in step without re-reading everything. |
Paging and sync
Every list returns { "items": [...], "nextCursor": "…", "hasMore": true }. Paging is keyset, never offset: pass nextCursor back as cursor and stop when it is null. It always becomes null, and a page is never silently cut short. limit is 1 to 200 (default 50).
cursor = null
repeat:
GET /public/leads?limit=200&updatedSince=<start of last run>[&cursor=<cursor>]
process items
cursor = response.nextCursor
until cursor == nullFor an incremental sync, store the time you started the last run and pass it as updatedSince (or createdSince) with a minute of overlap. Rows are not deduplicated for you, and seeing one twice is cheap. Unknown query parameters are ignored, and an empty one counts as absent, so blank optional fields in n8n are safe.
The change feed
GET /public/events (scope EVENTS_READ) is the endpoint to build on. Polling leads tells you a row changed; the feed tells you what happened, and it is the only way to learn about things that leave no row behind: merges, deletes and import undos.
curl "https://api.crm.diginetacademy.in/api/v1/public/events?since=19542&limit=100" \
-H "X-API-Key: $CRM_KEY"{
"items": [
{
"seq": 19543,
"apiVersion": "2026-09-12",
"eventType": "inquiry.won",
"leadId": "clx1lead0001",
"inquiryId": "clx1inq0001",
"data": { "fromStageCode": "negotiation", "toStageCode": "admitted" },
"suppressed": false,
"occurredAt": "2026-11-12T06:31:44.123Z"
}
],
"nextCursor": 19543,
"hasMore": false
}- First run: call it without
since; you get the last hour (sinceMinutes, default 60). - Process
itemsin order, then storenextCursor. Next time, send it assince; it is exclusive, so you never see a row twice. - An empty page means you are caught up: keep the cursor you have, wait a minute, and poll again.
hasMore: truemeans more is waiting, so poll again straight away. - Move the cursor only after the page is handled. Delivery is at least once, so handlers must be safe to run twice.
Events appear about two seconds after they happen, so a poller can never step over one that was still being saved. They are kept for 30 days. A cursor older than that gets 410 CURSOR_TOO_OLD with oldestSeq: set your cursor to oldestSeq - 1, then re-read leads and enquiries changed since the outage, because deletes in the lost window can’t be recovered. Bulk rows from imports and sweeps are hidden unless you ask for includeSuppressed=true.
Events carry ids, codes and before and after values, never a person’s name, phone or email. Read the person from GET /public/leads/{id} when you need them; a 404 there means they are gone.
| eventType | When |
|---|---|
lead.created | A new person enters the CRM, by any route. |
lead.updated | A person’s own fields change. data.changed names the fields, never the values. |
lead.merged | Two duplicates were merged; the source lead is gone. |
lead.deleted | An admin deleted the lead, or an import was undone. |
inquiry.created | A person shows interest in a campaign or course, including a returning lead’s second enquiry. |
inquiry.stage_changed | An enquiry moved along the pipeline: fromStageCode, toStageCode. |
inquiry.won | It entered a stage that converts. |
inquiry.lost | It entered a stage that closes without converting. |
inquiry.deleted | An admin deleted the enquiry, on its own or with its lead. |
payment.recorded | A payment was recorded against an enquiry. |
followup.due | A scheduled follow-up came due. |
import.completed | A CSV import finished (counts only; hidden unless includeSuppressed=true). |
leads.bulk_updated | A sweep changed many leads at once (hidden unless includeSuppressed=true). |
org.leads_wiped | Every lead in the organization was deleted: drop your copy and sync again. |
Filter with eventType=lead.created,inquiry.won (up to 20). A type the CRM doesn’t know matches nothing rather than failing. This is polling, not push: the CRM sends no webhooks.
Codes, phones and status
- Codes belong to each organization. Stages, call outcomes, campaigns, sources and courses are rows each business can rename, not fixed values. Read them from
GET /public/referencebefore branching on one. For open, converted or lost, use an enquiry’slifecycle, which keeps working when stages are renamed. - Phones.
phoneis stored as it came in (imports and ad forms write raw text), so it isn’t always dialable. Send tophoneE164, and skip the row when it is null. - A lead’s status follows its newest enquiry. A lead can read
NEWwhile holding an enquiry that converted. Check enquiries before messaging someone, so an admitted student isn’t sent a sales message. - Times are ISO 8601 in UTC. Show them in the organization’s timezone (
organization.timezoneinwhoami).
Errors
Every error has the same body. error is a stable code: branch on it, not on message.
{
"statusCode": 403,
"error": "SCOPE_DENIED",
"message": "This key does not have the LEADS_READ scope.",
"requiredScope": "LEADS_READ",
"requestId": "req_V1StGXR8Z5jdHi6B",
"path": "/api/v1/public/leads",
"timestamp": "2026-11-12T06:31:44.123Z"
}| Status | Codes | Retry? |
|---|---|---|
400 | VALIDATION_FAILED: issues lists every problem, not just the first. | No |
401 | MISSING_API_KEY, INVALID_API_KEY, KEY_EXPIRED, KEY_REVOKED | No |
403 | SCOPE_DENIED (requiredScope names what is missing), ORG_SUSPENDED, ORG_ARCHIVED | No |
404 | NOT_FOUND, including a record that belongs to another organization | No |
410 | CURSOR_TOO_OLD: the change feed only. You have missed events. | After resetting the cursor |
429 | RATE_LIMITED, ROW_RATE_EXCEEDED, ROW_BUDGET_EXCEEDED | Yes, after Retry-After |
500 | INTERNAL_ERROR | Yes |
Retry nothing else: a SCOPE_DENIED retried in a loop only burns your own limit.
Rate limits
Three separate limits, all per key, set when the key is made and changeable later by an admin. whoami and /public/reference cost no rows; every other endpoint counts one row for each row it returns.
| Limit | Default | Over it |
|---|---|---|
| Requests a minute | 120 (up to 600) | RATE_LIMITED |
| Rows a minute | 2,000 | ROW_RATE_EXCEEDED |
| Rows a day (India time) | 100,000 | ROW_BUDGET_EXCEEDED |
Every response says where you stand, so a client can slow down before it ever sees a 429:
| Header | Meaning |
|---|---|
X-Request-Id | This request’s id: quote it when you write to us. |
X-RateLimit-Limit | Your requests-a-minute limit. |
X-RateLimit-Remaining | Requests left in this rolling minute. |
X-RateLimit-Reset | Unix seconds when the request window rolls over. |
X-Row-Rate-Remaining | Rows left in this rolling minute. |
X-Row-Budget-Remaining | Rows left in today’s budget (a lower bound). |
Retry-After | On a 429 only: seconds to wait. |
The daily budget resets at midnight India time. The CRM tells the organization’s admins the first time a key passes 80% of its daily budget, which is how a leaked key gets noticed. Planning a one-off bulk export? Tell your admin first.
Connect Claude
Claude, Anthropic’s AI assistant, can read the CRM and answer questions in plain language: which hot leads nobody has called in three days, how each caller did this week, how much fee is still pending. It reads with the permissions of the person who connected it and can never change, delete, call or message anything.
- It is off for every organization until an owner turns it on in Integrations → Claude. Owners and admins can then connect; a manager can connect once an owner ticks them, and sees only their campaigns and team. Telecallers can’t.
- To connect, copy the connector URL from Integrations → Claude, then in Claude open Settings → Connectors → Add custom connector, paste it, sign in to the CRM and press Allow.
- Every question Claude asks is listed under Recent activity for 90 days. Disconnect ends a connection on its next question.