CazVid
API docs

CRM API

CRM API v1

Manage your CazVid CRM from code or an AI agent: read and search contacts, create and update them, log interactions, and attach resumes. This is the same contact database your team uses in the app - the API is a scoped, quota-limited surface over it.

Base URL

https://aio-backend-prod.cazvid.app/api/v3.0/crm

Overview

This reference covers twenty-two endpoints over four objects: contacts, their interaction timeline, resumes, and the hiring pipelines (lists) your contacts sit on as cards. Read endpoints use the crm:read scope; every write, including the pipeline routes, uses crm:write. Use /public/usage to check your remaining quota without spending it.

GET/public/contacts
List or search your contacts. Returns one page; withholds contact channels (see the privacy note).
GET/public/contacts/{id}
Read one contact in full, including its emails and phones.
POST/public/contacts
Create a contact. Idempotent: de-duplicates by email or phone within the workspace.
PATCH/public/contacts/{id}
Update fields on a contact. Send only the fields you want to change.
DELETE/public/contacts/{id}
Archive a contact (soft delete). Also archives its interactions.
POST/public/contacts/{id}/resume
Attach a resume (base64 PDF, DOC, or DOCX) and queue an asynchronous parse.
POST/public/contacts/{id}/interactions
Log an interaction (a timeline entry such as a call, interview, or note).
GET/public/contacts/{id}/interactions
Read a contact's interaction timeline, newest first.
GET/public/interaction-types
List the fixed global interaction-type names you can log, grouped by category.
GET/public/lists
List your pipelines, newest first, each with its active card count. Paginated on hasMore.
POST/public/lists
Create a pipeline. Seed your own stages or copy another pipeline's stage skeleton. Accepts an Idempotency-Key header.
GET/public/lists/{id}
Read one pipeline: its stages in board order, each with its card count and one page of cards.
PATCH/public/lists/{id}
Rename or recolor a pipeline. Stages are edited through the stage routes, not here.
DELETE/public/lists/{id}
Soft-delete a pipeline, its stages, and its cards, and report what the cascade removed.
POST/public/lists/{id}/stages
Add a stage at the end of the board. Returns the pipeline's full stage array in board order.
PATCH/public/lists/{id}/stages/{stageId}
Rename or recolor one stage. Board order is untouched.
PUT/public/lists/{id}/stages/order
Reorder the board. Send stageIds as the full desired order.
DELETE/public/lists/{id}/stages/{stageId}
Delete a stage, moving its cards to another stage of the same pipeline first. Returns the remaining stages as { stagesRemaining }.
POST/public/lists/{id}/items
Add an existing contact to a pipeline as a card.
PATCH/public/lists/{id}/items/{itemId}
Move a card to another stage of the same pipeline.
DELETE/public/lists/{id}/items/{itemId}
Remove a card from a pipeline. The contact itself is kept.
GET/public/usage
Read your daily quota. This call does not count against the quota.

The list withholds emails and phones

GET /public/contacts returns compact rows so an agent can browse and then open one contact. To protect against bulk harvesting, the list projection omits emails and phones (they come back as empty arrays) and returns companyId, locationGeoNameId, and updatedAt as null. Fetch GET /public/contacts/{id} for a single contact's full detail, including its channels. A contact that belongs to another workspace returns an indistinguishable 404, never a 403, so the API cannot be probed to learn whether a contact exists elsewhere.

Authentication

Send your API key with the x-api-key header or as an Authorization: Bearer token. This is the same CazVid Developer API key as the other API families - one key works across products, gated by scope. Keys belong to an organization; org owners and admins manage them in the app under Profile > Developer API. Plaintext keys are shown once when created and stored hash-only by CazVid. Treat keys as secrets: do not put them in frontend code, mobile apps, screenshots, logs, or support messages.

x-api-key: cazvid_jb_...
Authorization: Bearer cazvid_jb_...
Content-Type: application/json
The CRM API requires a Platinum, Diamond, or Enterprise plan. A key on a lower plan receives a 403 PLAN_REQUIRED; a key without the CRM scope receives a 403 SCOPE_FORBIDDEN. Org owners and admins manage keys from the Developer API section in CazVid.

Read contacts

GET /public/contacts takes an optional search (1 to 200 characters) that matches name, company, job title, skills, industry, education, email, and phone, plus page (1-based) and pageSize (up to 50, default 10). It paginates on hasMore only - there is no total count. jobTitle is an array: it carries every role from the contact's resume, so one contact can show several titles. Long strings are truncated at 150 characters with a ... [truncated] marker.

GET/public/contacts
List or search your contacts. Returns one page; withholds contact channels (see the privacy note).
curl "https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/contacts?search=logistics%20manager&pageSize=10" \
  -H "x-api-key: cazvid_jb_..."
{
  "items": [
    {
      "id": "6a57e240f55eb331f6e396c8",
      "name": "Sidney G.",
      "firstName": "Sidney",
      "lastName": "G.",
      "jobTitle": ["Logistics Manager", "Operations Coordinator"],
      "companyName": "Acme Freight",
      "companyId": null,
      "emails": [],
      "phones": [],
      "linkedinUrl": "https://www.linkedin.com/in/example",
      "locationName": "Bogota, Colombia",
      "locationGeoNameId": null,
      "tags": [{ "id": "6a1f0b2c9c7c630206ba1e77", "name": "priority" }],
      "hasResume": true,
      "resumeCount": 1,
      "cazvidUserId": null,
      "linkedCazvidUser": false,
      "createdAt": "2026-07-01T12:30:00.000Z",
      "updatedAt": null
    }
  ],
  "page": 1,
  "pageSize": 10,
  "hasMore": true
}

GET /public/contacts/{id} returns the full contact. On the list endpoint the fields marked below as list-withheld are empty or null by design.

GET/public/contacts/{id}
Read one contact in full, including its emails and phones.
FieldTypeDescription
idstringContact id. Use it with the single-contact, interaction, resume, update, and delete endpoints.
namestring | nullDisplay name.
firstNamestring | nullGiven name.
lastNamestring | nullFamily name.
jobTitlestring[]Array of job titles from the contact's resume; one contact can have several.
companyNamestring | nullCompany or employer name.
companyIdstring | nullLinked company id. List-withheld: always null on the list endpoint.
emails{ email, type }[]Email addresses with labels. List-withheld: empty on the list endpoint; populated on the single-contact endpoint.
phones{ phone, type }[]Phone numbers with labels. List-withheld: empty on the list endpoint; populated on the single-contact endpoint.
linkedinUrlstring | nullLinkedIn profile URL, when known.
locationNamestring | nullHuman-readable location.
locationGeoNameIdnumber | nullGeoNames id. List-withheld: always null on the list endpoint.
tags{ id, name }[]Workspace tags on the contact, each returned as an { id, name } object.
hasResumebooleanWhether the contact has at least one attached resume.
resumeCountnumberNumber of attached resumes.
cazvidUserIdstring | nullLinked CazVid platform user id, when the contact is a CazVid user.
linkedCazvidUserbooleanWhether the contact is linked to a CazVid platform user.
createdAtstring | nullISO 8601 creation timestamp.
updatedAtstring | nullISO 8601 last-update timestamp. List-withheld: always null on the list endpoint.
{
  "id": "6a57e240f55eb331f6e396c8",
  "name": "Sidney G.",
  "firstName": "Sidney",
  "lastName": "G.",
  "jobTitle": ["Logistics Manager", "Operations Coordinator"],
  "companyName": "Acme Freight",
  "companyId": "6a1f0b2c9c7c630206ba1e44",
  "emails": [{ "email": "sidney@example.com", "type": "work" }],
  "phones": [{ "phone": "+57 300 000 0000", "type": "mobile" }],
  "linkedinUrl": "https://www.linkedin.com/in/example",
  "locationName": "Bogota, Colombia",
  "locationGeoNameId": 3688689,
  "tags": [{ "id": "6a1f0b2c9c7c630206ba1e77", "name": "priority" }],
  "hasResume": true,
  "resumeCount": 1,
  "cazvidUserId": null,
  "linkedCazvidUser": false,
  "createdAt": "2026-07-01T12:30:00.000Z",
  "updatedAt": "2026-07-03T09:15:00.000Z"
}

Create, update, and archive contacts

Create with POST /public/contacts (only name is required), update with PATCH /public/contacts/{id} (send only the fields you want to change), and archive with DELETE /public/contacts/{id}. All three use the crm:write scope.

Create is idempotent

Create de-duplicates within the workspace by CazVid-user link, then email, then phone, so re-sending a contact that carries one of those returns the existing one (with deduplicated: true) instead of making a duplicate - no idempotency key is needed, and a retry after a timeout is safe. **Send an email or a phone if you intend to retry**: only name is required, but a name-only create has no de-duplication key, so retrying it after a timeout CAN create a second contact. deduplicated is advisory (a created-timestamp heuristic that can misreport when an existing contact's createdAt falls in the same millisecond as the request, or on legacy records with a missing or clock-skewed createdAt). If an email or phone matches more than one existing contact, the API refuses with 409 CONTACT_CONFLICT rather than guessing which one you meant; reconcile the duplicates and retry.

The create body accepts these fields. Update accepts the same set, all optional.

FieldTypeRequiredDescription
namestringRequiredDisplay name. The only required field on create.
firstNamestringOptionalGiven name.
lastNamestringOptionalFamily name.
jobTitlestring[]OptionalOne or more job titles (up to 10). Stored as the contact's jobTitle array.
companyNamestringOptionalCompany or employer name.
linkedinUrlstringOptionalLinkedIn profile URL.
locationNamestringOptionalFree-text location (city, region, or country).
locationGeoNameIdintegerOptionalGeoNames id for a normalized location, when known.
emails{ email, type? }[]OptionalEmail addresses as { email, type? } objects (type is work, personal, or other; up to 10 on create). On update, at most one - it replaces the primary email.
phones{ phone, type? }[]OptionalPhone numbers as { phone, type? } objects (type is mobile, desk, home, or other; up to 10 on create). On update, at most one - it replaces the primary phone.
tagsstring[]OptionalWorkspace tag names (up to 50). An unknown tag name creates that tag in the workspace.
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/contacts \
  -H "x-api-key: cazvid_jb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sidney G.",
    "companyName": "Acme Freight",
    "jobTitle": ["Logistics Manager"],
    "emails": [{ "email": "sidney@example.com", "type": "work" }],
    "phones": [{ "phone": "+57 300 000 0000", "type": "mobile" }]
  }'
{
  "id": "6a57e240f55eb331f6e396c8",
  "deduplicated": false,
  "name": "Sidney G.",
  "firstName": "Sidney",
  "lastName": "G.",
  "jobTitle": ["Logistics Manager"],
  "companyName": "Acme Freight",
  "companyId": null,
  "emails": [{ "email": "sidney@example.com", "type": "work" }],
  "phones": [{ "phone": "+57 300 000 0000", "type": "mobile" }],
  "linkedinUrl": null,
  "locationName": null,
  "locationGeoNameId": null,
  "tags": [],
  "hasResume": false,
  "resumeCount": 0,
  "cazvidUserId": null,
  "linkedCazvidUser": false,
  "createdAt": "2026-07-25T12:30:00.000Z",
  "updatedAt": "2026-07-25T12:30:00.000Z"
}
On update, emails and phones are capped to a single entry each: a PATCH replaces the contact's primary email or phone slot rather than appending, so send at most one of each. deduplicated is present only on the create response; it is omitted on update.
Delete is a soft archive, not a permanent erase: the contact is retained and recoverable, but it disappears from search and its interactions and list memberships are archived with it.

Resumes

POST /public/contacts/{id}/resume attaches a resume to a contact as an inline base64 file. fileName, mimeType, and contentBase64 are all required. It returns 202 Accepted and consumes one CRM daily call plus one of a separate per-key resume-upload quota (default 50 per UTC day; see the resumeUpload block in /public/usage).

POST/public/contacts/{id}/resume
Attach a resume (base64 PDF, DOC, or DOCX) and queue an asynchronous parse.
  • mimeType must be one of application/pdf, application/msword, or application/vnd.openxmlformats-officedocument.wordprocessingml.document (PDF, DOC, DOCX).
  • The decoded file must be at most 10 MB, and its real type is verified from its magic bytes against mimeType; a mismatch returns 400 MIME_MISMATCH, an empty file 400 EMPTY_FILE, and undecodable input 400 INVALID_BASE64.
  • Parsing is asynchronous: the 202 confirms the resume was queued. The parsed fields (name, email, phone, location, job title, skills, LinkedIn) are written onto the contact later, filling only its empty fields. Poll GET /public/contacts/{id} to see hasResume and resumeCount update.
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/contacts/6a57e240f55eb331f6e396c8/resume \
  -H "x-api-key: cazvid_jb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "fileName": "sidney-resume.pdf",
    "mimeType": "application/pdf",
    "contentBase64": "JVBERi0xLjc...=="
  }'

Interactions

An interaction is a timeline entry on a contact - a call, email, interview, offer, note, and so on. Log one with POST /public/contacts/{id}/interactions (type is required), read the timeline newest-first with GET /public/contacts/{id}/interactions, and discover the valid type names with GET /public/interaction-types. Logging is never de-duplicated: each call creates a new entry, so do not blindly retry a call that may already have succeeded. You can pass a future date to record a scheduled follow-up.

POST/public/contacts/{id}/interactions
Log an interaction (a timeline entry such as a call, interview, or note).
GET/public/contacts/{id}/interactions
Read a contact's interaction timeline, newest first.
GET/public/interaction-types
List the fixed global interaction-type names you can log, grouped by category.

The interaction taxonomy is fixed and global

type must be a name from a fixed global taxonomy that is the same for every workspace; an unknown name is rejected with 400 UNKNOWN_INTERACTION_TYPE, whose details lists every valid name. Call GET /public/interaction-types for the authoritative, current list rather than hardcoding them. The names are grouped into seven categories:

communicationassessmentinterviewsoffernextstepsotherrejected
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/contacts/6a57e240f55eb331f6e396c8/interactions \
  -H "x-api-key: cazvid_jb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "phonecall",
    "notes": "Left a voicemail about the operations role.",
    "date": "2026-07-04T16:00:00.000Z"
  }'

Lists and pipelines

A list is a hiring pipeline: a named board with ordered stages, holding one card per candidate. It is the same object the CazVid app calls a list, so a pipeline you build here shows up on the board your recruiters use, and their drag-and-drop moves are visible through these routes. Twelve endpoints cover the three levels: the pipeline itself, its stages, and its cards. Reads use crm:read; every write uses crm:write.

GET/public/lists
List your pipelines, newest first, each with its active card count. Paginated on hasMore.
POST/public/lists
Create a pipeline. Seed your own stages or copy another pipeline's stage skeleton. Accepts an Idempotency-Key header.
GET/public/lists/{id}
Read one pipeline: its stages in board order, each with its card count and one page of cards.
PATCH/public/lists/{id}
Rename or recolor a pipeline. Stages are edited through the stage routes, not here.
DELETE/public/lists/{id}
Soft-delete a pipeline, its stages, and its cards, and report what the cascade removed.
POST/public/lists/{id}/stages
Add a stage at the end of the board. Returns the pipeline's full stage array in board order.
PATCH/public/lists/{id}/stages/{stageId}
Rename or recolor one stage. Board order is untouched.
PUT/public/lists/{id}/stages/order
Reorder the board. Send stageIds as the full desired order.
DELETE/public/lists/{id}/stages/{stageId}
Delete a stage, moving its cards to another stage of the same pipeline first. Returns the remaining stages as { stagesRemaining }.
POST/public/lists/{id}/items
Add an existing contact to a pipeline as a card.
PATCH/public/lists/{id}/items/{itemId}
Move a card to another stage of the same pipeline.
DELETE/public/lists/{id}/items/{itemId}
Remove a card from a pipeline. The contact itself is kept.

Pipeline writes are workspace-wide

Any key scoped to a workspace can manage any pipeline in it, no matter which app user created it - there is no per-user ownership on these routes. The workspace boundary itself is absolute: a pipeline, stage, card, or contact in another workspace returns the same 404 NOT_FOUND as an id that does not exist, so the API cannot be probed to learn what exists elsewhere. The one narrower rule is copyFromListId on create, which in v1 also resolves only pipelines created by your own key's user; another member's pipeline reads as 404 even inside your workspace.

curl "https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/lists?pageSize=10" \
  -H "x-api-key: cazvid_jb_..."

Creating a pipeline takes an Idempotency-Key

POST /public/lists is the one CRM write with no natural de-duplication - two identical calls make two pipelines - so it accepts an optional Idempotency-Key header carrying any opaque string you can reproduce on a retry (a job-run id, a week stamp). A repeat with the same key AND the same body replays the first result for 24 hours, returning the original pipeline and its original id with an Idempotency-Replayed: true response header; a response without that header was created by that request. The same key with a different body is 422 IDEMPOTENCY_KEY_REUSED and will keep failing, so use a new key. A repeat sent while the first create is still running is 409 IDEMPOTENCY_IN_PROGRESS - nothing was duplicated, so retry in a moment for the replay. That in-flight claim is short-lived (about 2 minutes, not 24 hours): if the original request died mid-create, the claim expires on its own and the next retry creates the pipeline. A create that FAILS is never remembered, and the key is scoped per organization and resolved workspace.

curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/lists \
  -H "x-api-key: cazvid_jb_..." \
  -H "Idempotency-Key: weekly-pipeline-2026-W32" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Warehouse Hiring 2026",
    "color": "#0EA5E9",
    "stages": [
      { "name": "Sourced", "color": "#64748B" },
      { "name": "Phone Screen", "color": "#4F46E5" },
      { "name": "Offer", "color": "#16A34A" }
    ]
  }'

GET /public/lists returns every field below except updatedAt and stages. GET /public/lists/{id} adds those two, and applies the same page / pageSize window to EVERY stage, echoing them alongside hasMore at the top level. pageSize goes up to 50 and defaults to 10; page is 1-based and capped at 10000. hasMore is true when ANY stage has more cards, not just the largest one.

FieldTypeDescription
idstringPipeline id. Use it in every /public/lists/{id} path.
namestring | nullPipeline name as shown on the board.
colorstring | nullPipeline color, exactly as stored: a hex code or a plain color token.
templateTypestring | nullThe stage-template stamp. EVERY pipeline created through this API is stamped CANDIDATE_PIPELINE_EN, including one seeded from your own stages or copied from another board, so it does not tell you what the stages actually are; ones auto-created for a job posting carry a JOBSEARCH_PIPELINE_ value. Informational only, and it never changes after creation.
associatedPostsstring[]Ids of the job postings linked to this pipeline. Empty for a pipeline created through this API - posting links are made in the CazVid app.
itemCountnumberNumber of active cards on the pipeline, across every stage.
createdAtstring | nullISO 8601 creation timestamp.
updatedAtstring | nullISO 8601 last-change timestamp. Single-pipeline read only.
stagesStage[]The board's stages in board order (zorder ascending), each carrying id, name, color, zorder, its total itemCount, and one page of items. Single-pipeline read only.
{
  "id": "6a3ff55406ae2ed06ddb5fd2",
  "name": "Warehouse Hiring 2026",
  "color": "#0EA5E9",
  "templateType": "CANDIDATE_PIPELINE_EN",
  "associatedPosts": [],
  "itemCount": 1,
  "createdAt": "2026-08-01T10:00:00.000Z",
  "updatedAt": "2026-08-02T09:12:00.000Z",
  "page": 1,
  "pageSize": 10,
  "hasMore": false,
  "stages": [
    {
      "id": "6a3ff55406ae2ed06ddb5fe1",
      "name": "Sourced",
      "color": "#64748B",
      "zorder": 1,
      "itemCount": 1,
      "items": [
        {
          "id": "6a3ff55406ae2ed06ddb5fd1",
          "listId": "6a3ff55406ae2ed06ddb5fd2",
          "stageId": "6a3ff55406ae2ed06ddb5fe1",
          "contactId": "6a57e240f55eb331f6e396c8",
          "authorId": null,
          "rating": null,
          "applicationStatus": null,
          "disqualified": false,
          "disqualifiedReason": null,
          "source": "contact",
          "createdAt": "2026-08-01T10:05:00.000Z",
          "updatedAt": "2026-08-01T10:05:00.000Z"
        }
      ]
    }
  ]
}

A card carries ids, never personal data

The twelve fields below are everything a card exposes: ids plus pipeline state. There is no name, email, phone, or resume on a card by design. Pair the card's contactId with GET /public/contacts/{id} for those - that route is the only read that runs a linked candidate's own privacy check, and the board read deliberately does not repeat any of it. authorId is set when the applicant is a CazVid platform user, who may have no CRM contact record at all, in which case contactId is null.

Each card in a stage's items array carries exactly these twelve fields.

FieldTypeDescription
idstringCard id. Use it in the /items/{itemId} paths.
listIdstringId of the pipeline this card belongs to.
stageIdstring | nullId of the stage the card currently sits in.
contactIdstring | nullId of the CRM contact behind this card. Pair it with GET /public/contacts/{id} for the person's name, email, and phone.
authorIdstring | nullId of the CazVid platform user behind this card, set when the applicant is a CazVid user. Such a user may have no CRM contact record, in which case contactId is null.
rating"good_fit" | "maybe" | "not_a_fit" | nullRecruiter rating on the card, when one was set in the app. Not writable through this API in v1.
applicationStatus"invited" | "in_progress" | "completed" | "disqualified" | nullApplication status of the card, when one was set. Not writable through this API in v1.
disqualifiedbooleanWhether the card was disqualified.
disqualifiedReasonstring | nullFree-text reason recorded when the card was disqualified.
sourcestring | nullHow the card got onto the pipeline: contact for an API or CRM add, or an application source for a job applicant.
createdAtstring | nullISO 8601 timestamp the card was added to the pipeline.
updatedAtstring | nullISO 8601 timestamp the card last changed.
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/lists/6a3ff55406ae2ed06ddb5fd2/items \
  -H "x-api-key: cazvid_jb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "contactId": "6a57e240f55eb331f6e396c8",
    "stageId": "6a3ff55406ae2ed06ddb5fe1"
  }'
  • zorder is 1-based and contiguous. Every stage mutation renumbers the whole board, so zorder: 1 is always the stage new candidates land in.
  • A pipeline is capped at 30 active stages when you send your own stages or append one with POST /public/lists/{id}/stages. copyFromListId deliberately does NOT apply that cap, so copying a longer board gives you a pipeline over the limit - and one that can no longer be reordered through this API, since stageIds is itself capped at 30.
  • stages and copyFromListId are mutually exclusive on create (400 VALIDATION_ERROR when both are sent). Send neither and you get the default candidate pipeline. There is no language selector: build a Spanish board by sending your own stages, or by copying an existing Spanish one.
  • Name rules differ between a pipeline and its stages. A pipeline name accepts only alphanumeric words separated by single spaces, so an accent, hyphen, apostrophe, ampersand, period, or double space is 400 VALIDATION_ERROR. Stage names carry no such rule: they accept accented characters and slashes, which is how the seeded Spanish boards are named. So Contratación Almacén is rejected as a pipeline name but accepted as a stage name.
  • Adding a card needs an existing contact. POST /public/lists/{id}/items takes a contactId, not a person's details, so create the contact first. Adding the same contact twice never duplicates the card, but a repeat carrying a different stageId moves it - treat stage placement as an intentional write rather than a no-op.
  • stageId must belong to the pipeline in the path. Another pipeline's stage is 404 NOT_FOUND with details[0].field of stageId; a card can never be moved onto a foreign board. Adding a card to a pipeline that has no stages is 409 LIST_HAS_NO_STAGES - the id is valid, so add a stage rather than hunting for another pipeline.
  • Cards are never silently lost. Deleting a stage that holds cards requires ?moveToStageId, another active stage of the same pipeline; without it the call is 400 MOVE_TO_STAGE_REQUIRED, and a moveToStageId that is not an active stage of that pipeline is 404 NOT_FOUND naming moveToStageId. A pipeline's last remaining stage cannot be deleted at all (400 LAST_STAGE_UNDELETABLE), and a stage holding an extreme number of cards is 400 TOO_MANY_ITEMS_TO_REASSIGN.
  • PUT /public/lists/{id}/stages/order takes the full order: stageIds must be exactly the pipeline's current active stage ids, with no missing, extra, foreign, or repeated entry.
  • Every pipeline route consumes exactly one call from the same daily quota, burst limit, and per-IP backstop as the contacts and interactions routes. There is no separate pool and no per-route multiplier.
  • Deleting a pipeline reports its blast radius. The response carries effects with stagesDeleted and itemsDeleted, counted before the cascade runs. The cascade soft-deletes the pipeline, its stages, and its cards, and clears the pipeline link on the job posting that pointed at it - at most one, and not counted in effects, so re-read a posting if you need to confirm. The contacts themselves are untouched.
{
  "id": "6a3ff55406ae2ed06ddb5fd2",
  "deleted": true,
  "effects": {
    "stagesDeleted": 3,
    "itemsDeleted": 12
  }
}

Check remaining quota

GET /public/usage returns your daily CRM quota and the separate per-key resume-upload quota. It uses the same authentication as the other endpoints and does not count against the quota, so you can poll it before a batch to avoid 429 responses. Polling is throttled to 60 requests per minute per API key.

GET/public/usage
Read your daily quota. This call does not count against the quota.
curl https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/usage \
  -H "x-api-key: cazvid_jb_..."
FieldTypeDescription
dailyQuotanumberTotal successful CRM calls allowed per organization per UTC day (500).
usednumberQuota-consuming CRM calls made with this organization's API keys today (UTC).
remainingnumberSuccessful calls left before the quota resets.
resetsAtstring (ISO 8601)ISO 8601 timestamp of the next UTC midnight, when the quota resets.
period"utc_day"Quota window. Always utc_day in v1.
resumeUpload{ dailyLimit, used, remaining }The separate per-key resume-upload quota for today: dailyLimit (50), used, and remaining.
{
  "dailyQuota": 500,
  "used": 39,
  "remaining": 461,
  "resetsAt": "2026-07-26T00:00:00.000Z",
  "period": "utc_day",
  "resumeUpload": {
    "dailyLimit": 50,
    "used": 0,
    "remaining": 50
  }
}

Errors

Error responses use stable errorCode values so integrations do not need to parse prose messages. Every error echoes the request path; 429 responses add retryAfter, the seconds until the limit window resets. A 429 from the daily quota carries details.period of utc_day; a 429 from the per-minute burst or per-IP backstop carries minute.

StatusCodeMeaning
400VALIDATION_ERRORThe request failed validation (for example a malformed id, an out-of-range page or page size, a pipeline name with an accent or punctuation, or a body field that is not on the allowlist). details names each offending field.
400UNKNOWN_INTERACTION_TYPEThe type on a logged interaction is not in the global taxonomy. details lists every valid name.
400MIME_MISMATCHThe uploaded file's real type does not match the declared mimeType.
400EMPTY_FILEThe uploaded resume decoded to an empty file.
400INVALID_BASE64The contentBase64 could not be decoded.
400MOVE_TO_STAGE_REQUIREDA delete was sent for a stage that still holds cards without ?moveToStageId. The call is rejected before anything is written - cards are never silently deleted.
400LAST_STAGE_UNDELETABLEThe pipeline's only remaining stage cannot be deleted. A pipeline always keeps at least one.
400TOO_MANY_ITEMS_TO_REASSIGNThe stage holds more cards than one request may reassign. Move some of them to another stage first.
401API_KEY_MISSINGNo API key was sent.
401API_KEY_INVALIDThe key does not exist or cannot be verified.
401API_KEY_EXPIREDThe key is past its expiry date.
401API_KEY_REVOKEDThe key was revoked.
403PLAN_REQUIREDThe CRM API requires a Platinum, Diamond, or Enterprise plan.
403SCOPE_FORBIDDENThe key lacks the required CRM scope (crm:read for reads, crm:write for writes).
403ORG_ACCESS_DENIEDThe key cannot access this organization.
403WORKSPACE_ACCESS_DENIEDThe workspaceId in the request does not belong to your API key.
404NOT_FOUNDNo contact, pipeline, stage, or card with that id exists in the resolved workspace. One in another workspace returns this same 404. On the pipeline routes, details[0].field names which id to fix (id, stageId, itemId, contactId, or moveToStageId).
409CONTACT_CONFLICTThe email or phone matches more than one existing contact, so it cannot be resolved automatically. Reconcile the duplicates and retry.
409LIST_HAS_NO_STAGESA card was added to a pipeline with no active stage for it to land on. Add a stage, then retry.
409IDEMPOTENCY_IN_PROGRESSA POST /public/lists with this Idempotency-Key is still running. Nothing was duplicated; retry shortly for the replay.
413PAYLOAD_TOO_LARGEThe decoded resume exceeds the 10 MB limit.
422IDEMPOTENCY_KEY_REUSEDThe Idempotency-Key was already used with a different request body. Use a new key; retrying will not help.
429RATE_LIMIT_EXCEEDEDA rate limit was hit: the daily quota (details.period utc_day) or the per-minute burst or per-IP backstop (details.period minute).
500CRM_API_ERRORAn unexpected server error occurred, or a dependency was unavailable.
{
  "statusCode": 429,
  "errorCode": "RATE_LIMIT_EXCEEDED",
  "message": "CRM API burst limit exceeded. Slow down and retry shortly.",
  "details": {
    "limit": 30,
    "period": "minute"
  },
  "retryAfter": 43,
  "path": "/api/v3.0/crm/public/contacts"
}

Limits and behavior

  • The CRM daily quota is 500 successful calls per organization per UTC day. This is a separate quota from the other API families. Every metered call (reads and writes) consumes one; GET /public/usage is free.
  • A per-organization burst limit of 30 calls per minute, plus a shared 60-per-minute per-IP backstop, protect the backend. Exceeding either returns 429 with details.period of minute.
  • Resume uploads have their own quota of 50 per API key per UTC day, on top of the daily call slot each upload also consumes.
  • The list endpoint withholds emails, phones, companyId, locationGeoNameId, and updatedAt; fetch a single contact for those. jobTitle is an array, and long strings truncate at 150 characters.
  • A contact in another workspace returns an indistinguishable 404. Rotate keys by creating a new key, updating your integration, then revoking the old key.
  • The pipeline routes share that one pool: each consumes a single daily call, and their writes are workspace-wide (any key scoped to the workspace can manage any pipeline in it). A pipeline, stage, or card in another workspace returns the same indistinguishable 404.

Use from an agent (MCP)

Prefer to drive your CRM from an AI agent instead of raw HTTP? The CazVid MCP server exposes the nine crm_* tools (search, get, upsert, update, delete contact, log and list interactions, list interaction types, and upload resume) plus list_api_usage to Claude Code, Claude.ai, ChatGPT, Cursor, and VS Code, using this same API key and quota.

Read the MCP server docs