Docs: REST API

Zyflow REST API

Your scripts and forms write to the same customers your AI app does, under their own name. Every write comes back with a batchId you can undo.

Last updated Oct 3, 2026

This is the developer guide to the Zyflow REST API, the way in for your own code, next to the Model Context Protocol (MCP) server your AI app uses. Zyflow is a lightweight small-business CRM your AI app keeps up to date (Claude, Cursor, ChatGPT Business, any MCP app). Every AI change is labeled.

Yes, Zyflow has an API. The Zyflow REST API lets your own code read and write the same customers, deals, follow-ups, money owed, files and documents your AI app uses through the MCP server. Send a named API key as a Bearer token to https://app.zyflowcrm.com/api/v1, from a server. It’s on every plan, and calls are never billed.2

Connecting an AI app? Use your Zyflow link instead. See the MCP server overview.

Create an API key

You manage keys on the Connect page.

Read the OpenAPI reference

Base URL and authentication

Every call goes to https://app.zyflowcrm.com/api/v1, sends and receives JSON, and carries an API key in the Authorization header as a Bearer token. A key belongs to one workspace. The REST API takes API keys only; OAuth sign-in is for AI apps on the MCP server.

If one login runs several businesses (up to 10 workspaces per login), create a key in each workspace.

curl "https://app.zyflowcrm.com/api/v1/contacts?limit=5" \
  -H "Authorization: Bearer $ZYFLOW_API_KEY"

Without a valid key, you get 401:

{"error":true,"code":"unauthenticated","message":"Sign in, or send your API key as \"Authorization: Bearer zf_live_...\".","status":401}

The same key also works as a Bearer token on the MCP server, for AI apps that can’t sign in. See Connect any MCP app.

Create and manage API keys

Create Zyflow API keys on the Connect page. Choose New key, name it after where it’s used, like “Website form”, and pick Read and write or Read only. The key starts with zf_live_ and is shown once. Changes made with it appear in History under that name, and you can revoke it instantly.

Names can be up to 60 characters. Copy the key when it appears: Zyflow keeps only a hash, so it can’t be shown again. The panel lists each key by name, first characters and last use, and revoking is instant: “Anything using this key stops working immediately.”

Active keys: Free 3, Solo 10, Team 50, Pro 200. At the limit, a new key answers 402 api_key_limit. Revoke one you no longer use.

Read and write vs Read only
Access Read, list and search Export Create, edit, delete or undo
Read and write Yes Yes Yes
Read only Yes Yes No. Refused before anything runs, with 403 insufficient_scope

Before you hand out a key

  • Keys don’t expire. Revoke any key you stop using.
  • Any member can create a key, and can list or revoke anyone’s.
  • A key acts as an agent named after it, with its creator’s role. Every member, and every member’s AI, can see and edit every customer. There are no per-person permissions yet.
  • If its creator leaves the workspace, the key stops working (403 not_a_member). It isn’t revoked, though, and would work again if they rejoined. Revoke it yourself.
  • Creating and revoking keys show in History but can’t be undone.
  • Keys reach /api/v1 only. Account, team, billing and key routes answer 403 api_key_not_allowed.
Create an API key

You manage keys on the Connect page.

Keep your key on a server

Don’t call the Zyflow API from browser code. Cross-origin requests are accepted only from Zyflow’s own sites, and any key, even a read-only one, can export your records, documents and history. Keep the key in a server or serverless function, and have your page post to that.

  • Never put a key in browser code, a mobile app or a public repo.
  • Store it in an environment variable or your host’s secret store.
  • Use one key per integration, so you can revoke one without breaking the others.

The OpenAPI reference

The full reference is generated from the code: Swagger UI at https://app.zyflowcrm.com/api/docs and the OpenAPI document at https://app.zyflowcrm.com/api/docs/json (“Zyflow API” v1.0.0). This page covers keys, conventions and examples. The reference lists every /api/v1 route, field and response.

Only /api/v1 paths accept keys. Routes are grouped by contacts, companies, deals, memories, activities, tasks, files, documents, business, insights, history, settings and data.

Don’t mix up the two addresses: this guide is /docs/api on the Zyflow site, and the generated reference is /api/docs on the app.

Conventions

Every route follows the same rules: typed ids, one change per write request, version checks, cursor paging, and saves that find the record you already have.

Typed ids

A type prefix plus 12 characters, with no look-alike symbols: ct_ contact, co_ company, dl_ deal, ac_ activity, mem_ memory, tk_ task, doc_ document, tpl_ template, fl_ file, dir_ folder, bat_ change batch.

One request, one change

Each write request runs in one transaction, so it saves in full or not at all, and returns a batchId (MCP calls it change_id). Uploads and document writes use their own short transactions around the file or the PDF render.

Versions

Records carry a version. PATCH on contacts, companies, deals and documents accepts expectedVersion; if the record changed since you read it, you get 409 version_conflict with the current record in details.current. Tasks have no version.

Paging and search

Lists return items, nextCursor and total. limit defaults to 25, up to 200; send nextCursor back as cursor. deleted=true lists the trash. Search returns up to 50 results.

Saving without duplicates

A plain POST /api/v1/contacts always creates a contact. With ?upsert=true, Zyflow finds the record you already have (by email, then phone, then exact name) and answers action: created, updated, unchanged or ambiguous. ?dryRun=true previews without writing; ?createNew=true forces a new record. Companies work the same way.

Names as people say them

customer: “Maya at Northwind” works where an id would, and GET /api/v1/resolve?q= looks a name up. An unclear name returns 409 ambiguous_reference with details.candidates, and your code picks the id.

Forgiving writes

A bad value, such as an invalid phone, is skipped with a line in warnings, and the rest saves.

Dates

Send YYYY-MM-DD. Numeric dates like 03/10 follow the workspace’s country. Phrases like “tomorrow” work in English, in the workspace’s time zone.

Errors

Every error has the same JSON shape: error, code, message, status and, sometimes, details. Branch your code on code, and show message to a person: each one says what went wrong and what to do next.

{"error":true,"code":"insufficient_scope","message":"This connection is read-only. Reconnect with write access, or use an API key that has crm:write.","status":403}
Error codes you’ll meet
Status code What to do
400 validation_failed Fix the field named in message. details lists up to 5 problems
401 unauthenticated Send Authorization: Bearer zf_live_…
402 record_limit, api_key_limit, history_limit A plan limit: free up records, revoke an unused key, or the change is older than the history window
403 insufficient_scope The key is Read only. Use a Read and write key
403 insufficient_role The route needs an owner or admin, and the key’s creator isn’t one
403 api_key_not_allowed Keys work on /api/v1 only
403 not_a_member The key’s creator left the workspace. Create a new key
404 not_found Check the id and that the key belongs to the right workspace
404 documents_off Documents are switched off in this workspace
409 version_conflict Read the record again (it’s in details.current) and retry
409 ambiguous_reference Pick an id from details.candidates
409 already_undone Nothing to do. That change was already undone
413 payload_too_large Send a smaller body or file
429 rate_limited Wait the seconds in Retry-After, then retry

Cost and rate limits

REST API calls are never billed, and there are no AI or API credits.2 Short burst limits per key stop runaway scripts. Over the limit, you get 429 with a Retry-After header.

History and undo

Every write returns a batchId. To reverse it, POST to /api/v1/changes/{batchId}/undo. Everything that request changed goes back in one step, fields edited again since are kept, and the undo is itself a change you can undo.1

curl -X POST "https://app.zyflowcrm.com/api/v1/changes/bat_…/undo" \
  -H "Authorization: Bearer $ZYFLOW_API_KEY"

Response:

{"batchId":"bat_…","undone":[…],"skipped":[]}

The answer lists undone and skipped items (each skip with a reason) and its own batchId. You get 404 for a batch that isn’t in this workspace, 409 already_undone, or 402 history_limit when the change is older than your plan’s history window.

Find changes

GET /api/v1/changes returns batches (each with undoable and undoneAt) and hasMore. Filter by entityId, entityType, via (app, mcp, api, import or system), clientName (a key’s name) and since (an ISO date and time). It pages with limit, up to 100, and offset, not a cursor.

In the app, your code’s changes show in History under the API tab with the key’s name, and in “What your AI did” as “via API”.

Undo can’t reverse settings changes, creating or revoking keys, files deleted for good or an emptied trash, or deleting a workspace.

Documents: invoices, quotes and letters

POST /api/v1/documents makes an invoice, quote or letter from a template. Zyflow numbers it when it’s issued, works out totals and tax, renders the PDF and files it. To record a payment, post an amount to /documents/{id}/status. Zyflow doesn’t send documents to customers or collect payments.

  • POST /api/v1/documents: kind (invoice, quote or document), contactId or companyId, items, currency, dueInDays.
  • POST /api/v1/documents/preview: the document as it would print. Nothing is saved.
  • GET /api/v1/documents/{id}: an id or a number, such as “INV-0007”.
  • PATCH /api/v1/documents/{id}: replaces the lines as a whole and remakes the PDF.
  • POST /api/v1/documents/{id}/status: issue, mark paid, accepted or declined, void or reopen. Add amount to record a payment.
  • POST /api/v1/documents/{id}/convert turns a quote into an invoice; /duplicate copies it into a new draft (REST only); /render remakes the PDF. GET /api/v1/documents/{id}/pdf redirects to the PDF.
  • Templates (Liquid HTML): /api/v1/document-templates.

Issued invoices and quotes are voided, not deleted. With documents switched off, these routes answer 404 documents_off.

Zyflow works out totals, tax and numbering. Unit prices come from what you or your AI enter; there’s no price list. Zyflow records payments; it doesn’t collect them, and there are no card payment links. Zyflow sends nothing to your customers.

Export your data from code

GET /api/v1/export/{type} returns contacts, companies, deals, tasks, activities or memories as CSV, or workspace.json with your settings, records, documents, change history (up to 100,000 changes) and links to every file. File contents download separately. Any key can export, including a read-only one.3

Types: contacts.csv, companies.csv, deals.csv, tasks.csv, activities.csv, memories.csv and workspace.json. Zyflow runs no scheduled backups, so schedule your own with a Read only key named “Nightly export”.

curl "https://app.zyflowcrm.com/api/v1/export/workspace.json" \
  -H "Authorization: Bearer $ZYFLOW_API_KEY" \
  -o "zyflow-$(date +%F).json"

What the REST API doesn’t do

Zyflow has no webhooks, event subscriptions or official SDKs. To pick up changes, poll GET /api/v1/changes with since, or list records with updatedAfter. REST has no idempotency keys, so save contacts with ?upsert=true to land on the record you already have.

A poll every few minutes (store the time of your last check):

curl "https://app.zyflowcrm.com/api/v1/changes?since=2026-10-03T07:00:00Z&limit=100" \
  -H "Authorization: Bearer $ZYFLOW_API_KEY"

There are no native Zapier, Make or n8n apps. A tool with a generic HTTP step can call the API from its own servers, with the key stored in that tool’s credentials.

How the REST API differs from the MCP server

Both reach the same records. AI apps use the MCP server: they sign in with OAuth and call tools shaped around tasks. Your code uses the REST API with an API key and routes shaped around records. A few behaviors differ:

REST API and MCP server, side by side
Topic REST API MCP server
Sign-in API key OAuth sign-in, or an API key
Shape Routes around records Tools around tasks
Retry keys None idempotency_key, kept 24 hours
Repeat guards (same message within 15 minutes, matching open follow-up) Yes Yes
Bulk changes, up to 200 records Preview only when you send dryRun: true Preview by default
Permanent deletes A Read and write key can delete trashed files for good (?permanent=true) and empty the file trash (DELETE /api/v1/trash) AI apps can only move things to the trash
Call log Reads aren’t logged Every call logged by tool name
Sizes Lists up to 200, search up to 50 Lists up to 100, search up to 10
Log a call and set its follow-up Two requests One tool call

Examples

Create or update a contact

Run this once for 201 and created. Run it again for 200 and updated or unchanged, on the same contact.

curl -X POST "https://app.zyflowcrm.com/api/v1/contacts?upsert=true" \
  -H "Authorization: Bearer $ZYFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Maya Chen","company":"Northwind","emails":["maya@northwind.example"],"source":"Website form","tags":["website"]}'

Response (trimmed):

{
  "action": "created",
  "record": { "id": "ct_…", "type": "contact", "fullName": "Maya Chen", "version": 1, "clientName": "Website form", … },
  "related": { "company": { "action": "created", "id": "co_…", "name": "Northwind" } },
  "warnings": [],
  "batchId": "bat_…"
}
  • Passing company creates Northwind if it doesn’t exist yet, and that company counts as a record.4
  • When action is unchanged or ambiguous, nothing was written, so there’s nothing to undo.

A website form through a small server-side relay

Your page posts the form to a small function on your server that holds the key and makes three calls: save the contact, log the message, set a follow-up. It needs Node 18 or newer and reads ZYFLOW_URL (your Zyflow address) and ZYFLOW_API_KEY from the environment. Wire handleFormPost to your host’s request handler.

// Runs on your server, never in the browser. Node 18 or newer.

const BASE = `${process.env.ZYFLOW_URL}/api/v1`;
const KEY = process.env.ZYFLOW_API_KEY; // a "Website form" key, Read and write

async function zyflow(path, body) {
  const res = await fetch(BASE + path, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
  });
  const data = await res.json();
  if (res.ok === false) throw new Error(`${data.code}: ${data.message}`);
  return data;
}

export async function handleFormPost(form) {
  if (form.website) return { ok: true }; // honeypot: people leave this hidden field empty
  const name = String(form.name ?? "").trim().slice(0, 160);
  const email = String(form.email ?? "").trim().slice(0, 254);
  const message = String(form.message ?? "").trim().slice(0, 5000);
  if (name === "" || message === "" || email.includes("@") === false) {
    return { ok: false, error: "Please fill in your name, email and message." };
  }

  // 1. Land on the contact you already have, matched by email
  const saved = await zyflow("/contacts?upsert=true", {
name, emails: [email], source: "Website form", tags: ["website"],
  });

  // 2. More than one contact could match: stop, and keep the candidates for a person
  if (saved.action === "ambiguous") return { ok: true, review: saved.candidates };
  const contactId = saved.record.id;

  // 3. Log the message on that contact
  await zyflow("/activities", { contactId, kind: "message", title: "Website form", body: message });

  // 4. Optional: a follow-up for tomorrow, in the workspace's time zone
  await zyflow("/tasks", { contactId, title: `Reply to ${name}'s website message`, due: "tomorrow" });

  return { ok: true };
}
  • Each request is its own change, with its own Undo in History.
  • A retried post lands on the same contact. The same message on the same customer within 15 minutes, or a matching open follow-up, comes back duplicate: true and isn’t saved again.
  • The relay is public: validate and length-limit every field, and keep the honeypot.
  • The “Website form” key (Read and write) can do more than this relay needs, including exporting the workspace and deleting trashed files for good. Keep it in your host’s secret store, and call only these three routes.

Spot an error? Tell us

  1. 1 Undo works within your plan’s history window: 30 days on Free, up to 3 years on paid plans. Deleted items can be restored for 30 days on every plan. Settings changes, such as pipelines, custom fields and tag renames, are logged but can’t be undone. ↩
  2. 2 Your AI app’s calls to Zyflow, through MCP or the REST API, are never billed on any plan, and there are no AI credits. Short burst limits stop runaway scripts. Your AI app’s own subscription and usage limits still apply. ↩
  3. 3 Export gives you six CSV files, or one JSON file with your records, documents, change history (up to 100,000 changes) and links to every file. File contents download separately from Files. ↩
  4. 4 Records are contacts, companies and deals. Notes, calls, memories, follow-ups, files, invoices and quotes don’t count, and neither does anything in the trash. Importing contacts with a company column also creates those companies, so a sheet can use more records than it has rows. ↩

Last updated Oct 3, 2026

Create your first key

Name it after where it’s used, choose Read only if your code only reads, and keep it on a server. Every change it makes carries its name.

Create an API key

You manage keys on the Connect page.