Zapier Integration API
Last updated: July 11, 2026
This page documents the public API surface that powers the HelmIQ integration on Zapier: how a user connects their account, every trigger, action, and search the integration exposes, and how the API reports errors and rate limits. It is the reference for the published Zapier Platform app and for anyone wiring HelmIQ into a custom Zap.
Overview
HelmIQ is a CRM built for advisory and dealmaking teams. The Zapier integration lets any of the 9,000+ apps on Zapier read from and write to a connected HelmIQ organization: create and edit contacts, companies, deals, tasks, and notes; look up existing records; and fire Zaps when records change. Every request acts inside the connecting user's active organization and is strictly scoped to that organization on the server, so one connection can never see or touch another firm's data.
All endpoints live under https://helmiq.net/api/integrations/zapier. Requests carry a Bearer access token obtained through OAuth (below). Triggers are GET; actions and searches are POST with a JSON body. Responses are JSON.
Authentication
The integration authenticates with OAuth 2.0 authorization code + PKCE (S256)against HelmIQ's OAuth 2.1 server. It is a public client with no client secret: HelmIQ advertises token_endpoint_auth_methods_supported: ["none"], so PKCE is the proof-of-possession instead of a secret.
Connecting an account
- The user clicks Connect in Zapier. Zapier redirects the browser to the authorize endpoint with a generated
code_challengeandcode_challenge_method=S256. - The user signs in to HelmIQ and approves the connection. HelmIQ redirects back to Zapier with an authorization
code. - Zapier exchanges the code (plus the PKCE
code_verifier) for an access token and refresh token at the token endpoint. - Zapier calls the connection-test endpoint to label the connection, for example Salt Creek Advisory (jack@firm.com).
Endpoints
| Field | Type | Notes |
|---|---|---|
| GET /api/oauth/authorize | authorize | Params: response_type=code, client_id, redirect_uri, state, scope, code_challenge, code_challenge_method=S256. |
| POST /api/oauth/token | token | grant_type=authorization_code with code + code_verifier + client_id + redirect_uri. Returns access_token, token_type, expires_in, refresh_token, scope. |
| POST /api/oauth/token | refresh | grant_type=refresh_token with refresh_token + client_id. Returns a rotated access + refresh pair (refresh tokens are single-use). |
| GET /api/integrations/zapier/me | connection test | Returns { ok, orgId, orgName, userId, userEmail }. Not gated on feature flags, so a firm can connect before enabling actions/triggers. |
The granted scope is mcp:read mcp:write. Every authenticated request sends Authorization: Bearer <access_token>. Access tokens are short-lived (about one hour); Zapier refreshes them automatically on a 401.
Triggers
Triggers are polling. Each is a GET that returns records changed (or created) since an opaque cursor. The response shape is { ok, items: [...], cursor }. Zapier passes the returned cursor back on the next poll via ?cursor=<opaque>; omit it on the first poll. The cursor encodes a stable (timestamp, id) tuple so rows that share a millisecond are never skipped. Pages hold up to 50 items. Triggers require the firm's zapierTriggersEnabled flag. Soft-deleted records are never returned.
Contact Updated
GET https://helmiq.net/api/integrations/zapier/triggers/contact-updated
Fires for each contact whose updatedAt advanced since the cursor. Cursor key: (updatedAt, id) ascending.
| Field | Type | Notes |
|---|---|---|
| id | string | Contact id (primary key). |
| firstName | string | null | Given name. |
| lastName | string | null | Family name. |
| string | null | Primary email, lowercased on write. | |
| title | string | null | Job title. |
| companyId | string | null | Linked company id, when known. |
| createdAt | string (ISO 8601) | Creation timestamp. |
| updatedAt | string (ISO 8601) | Last-modified timestamp. |
Deal Updated
GET https://helmiq.net/api/integrations/zapier/triggers/deal-updated
Fires for each deal whose updatedAt advanced since the cursor. Cursor key: (updatedAt, id) ascending.
| Field | Type | Notes |
|---|---|---|
| id | string | Deal id (primary key). |
| name | string | Deal name. |
| stage | string | Pipeline stage (e.g. prospect). |
| dealType | string | Deal type (e.g. ma_sellside). |
| value | number | null | Deal value. |
| companyId | string | null | Linked company id. |
| createdAt | string (ISO 8601) | Creation timestamp. |
| updatedAt | string (ISO 8601) | Last-modified timestamp. |
New Deal
GET https://helmiq.net/api/integrations/zapier/triggers/new-deal
Fires for each newly created deal. Cursor key: (createdAt, id) ascending. Supports an optional ?format=arraymode that returns a bare array of the newest deals (no cursor) for Zapier's generic "Retrieve Poll" webhook, which dedups by each item's id.
| Field | Type | Notes |
|---|---|---|
| id | string | Deal id (primary key). |
| name | string | Deal name. |
| stage | string | Pipeline stage (e.g. prospect). |
| dealType | string | Deal type (e.g. ma_sellside). |
| value | number | null | Deal value. |
| companyId | string | null | Linked company id. |
| createdAt | string (ISO 8601) | Creation timestamp. |
| updatedAt | string (ISO 8601) | Last-modified timestamp. |
New Reminder
GET https://helmiq.net/api/integrations/zapier/triggers/new-reminder
Fires for each newly created reminder (task). Cursor key: (createdAt, id) ascending.
| Field | Type | Notes |
|---|---|---|
| id | string | Task (reminder) id. |
| title | string | Reminder title. |
| description | string | null | Reminder body. |
| dueDate | string (ISO 8601) | null | Due date, when set. |
| status | string | Status (e.g. pending). |
| contactId | string | null | Linked contact id. |
| dealId | string | null | Linked deal id. |
| createdAt | string (ISO 8601) | Creation timestamp. |
| updatedAt | string (ISO 8601) | Last-modified timestamp. |
Actions
Actions are POSTwith a JSON body. The organization and author are always taken from the access token, never from the body, so a Zap can only write into the connected organization. Actions require the firm's zapierActionsEnabled flag. Every action is idempotent: a Zap that fires twice with the same input returns the existing record rather than stacking a duplicate. Success responses include { ok: true, ... , created: boolean }, where created=false means an existing record was returned.
Create Contact
POST https://helmiq.net/api/integrations/zapier/actions/create-contact
| Field | Type | Notes |
|---|---|---|
| string (required) | Must be a valid email. | |
| firstName | string | Optional. |
| lastName | string | Optional. |
| title | string | Optional job title. |
| phone | string | Optional. |
| companyNameCandidate | string | Optional company name to link or create. |
| linkedinUrl | string | Optional. |
Idempotency: goes through the canonical contact-creation path. If a contact with the same email already exists in the organization, that contact is returned with created: false. Returns { ok, id, contactId, companyId, created }. A rejected payload (for example a role-account address that fails the shape gate) returns 422 with a reason.
Create Company
POST https://helmiq.net/api/integrations/zapier/actions/create-company
| Field | Type | Notes |
|---|---|---|
| name | string (required) | Cleaned (LLC/Inc. suffixes stripped) before dedup and write. |
| domain | string | Optional. |
| website | string | Optional. |
Idempotency: dedup key is (organizationId, cleaned name), case-insensitive. An existing company is returned with created: false. Returns { ok, id, companyId, created }.
Create Deal
POST https://helmiq.net/api/integrations/zapier/actions/create-deal
| Field | Type | Notes |
|---|---|---|
| name | string (required) | Deal name. |
| stage | string | Defaults to prospect. |
| dealType | string | Defaults to ma_sellside. |
| value | number | Optional deal value. |
| ebitda | number | Optional. |
| companyId | string | Optional; verified to belong to the org (404 if not). |
| externalSource | string | Upstream system name; part of the dedup key. |
| externalId | string | Upstream record id; part of the dedup key. |
| description | string | Optional. |
Idempotency: when both externalSource and externalId are supplied, the dedup key is (organizationId, externalSource, externalId); a resync of the same upstream record returns the existing deal with created: false (a concurrent duplicate is caught by a unique index and resolved the same way). Returns { ok, id, dealId, created }.
Create Task
POST https://helmiq.net/api/integrations/zapier/actions/create-task
| Field | Type | Notes |
|---|---|---|
| title | string (required) | Task title. |
| description | string | Optional. |
| contactId | string | Optional; verified org-scoped (404 if not). |
| dealId | string | Optional; verified org-scoped (404 if not). |
| companyId | string | Optional; verified org-scoped (404 if not). |
| dueDate | string (date) | Optional. |
Idempotency: when a contactId is present, the dedup key is (organizationId, contactId, title, status=pending), case-insensitive; a matching open task is returned with created: false. Tasks with no contact are never deduped (two distinct standalone reminders sharing a title must both persist). Returns { ok, id, taskId, created }.
Add Note
POST https://helmiq.net/api/integrations/zapier/actions/add-note
| Field | Type | Notes |
|---|---|---|
| contactId | string | Provide exactly one of contactId or contactEmail. |
| contactEmail | string | Resolved org-scoped, case-insensitive. |
| content | string (required) | The note body. |
| dealId | string | Optional; verified org-scoped. |
| meetingTitle | string | Optional. |
| meetingDate | string (ISO 8601) | Optional; parsed null-safe. |
| transcript | string | Optional full transcript. |
| externalDedupKey | string | Optional caller idempotency key; derived from contact + date + content when absent. |
Idempotency: dedup key is (organizationId, source=zapier, externalDedupKey). A re-delivery returns the existing note with created: false, duplicate: true and a 200 (so a re-fired Zap shows green). An unmatched contact or deal returns 404. Returns { ok, noteId, contactId, created, duplicate }.
Edit Contact
POST https://helmiq.net/api/integrations/zapier/actions/edit-contact
| Field | Type | Notes |
|---|---|---|
| contactId | string (required) | Must belong to the connected org (404 if not). |
| firstName | string | Optional; only supplied fields are updated. |
| lastName | string | Optional. |
| title | string | Optional. |
| phone | string | Optional; normalized to the org phone region. |
| string | Optional; lowercased on write. | |
| linkedinUrl | string | Optional. |
Partial update (PATCH semantics): only the fields present in the body are touched. Changing the email to one another contact in the org already holds returns 409 duplicate_email with the colliding existingContactId. Returns { ok, contactId }.
Edit Deal
POST https://helmiq.net/api/integrations/zapier/actions/edit-deal
| Field | Type | Notes |
|---|---|---|
| dealId | string (required) | Must belong to the connected org (404 if not). |
| name | string | Optional; only supplied fields are updated. |
| stage | string | Optional; a real change resets the days-in-stage clock. |
| value | number | Optional. |
| ebitda | number | Optional. |
| description | string | Optional. |
| expectedCloseDate | string (date) | Optional; an invalid date returns 400, empty string clears it. |
Partial update: only supplied fields are touched. A deal in another org or a deleted deal returns 404. Returns { ok, dealId }.
Searches
Searches power Zapier's find-or-create pattern. Each is a POST that returns a bare JSON array of matches (up to 5), org-scoped. A search with no match returns 200 with an empty array [], which is what makes Zapier fall through to the paired Create action. A search with no usable input also returns 200 [] (it never errors), while a malformed payload returns 400. Searches require the zapierActionsEnabled flag.
Find Contact
POST https://helmiq.net/api/integrations/zapier/searches/find-contact
| Field | Type | Notes |
|---|---|---|
| string (required) | Matched case-insensitively, org-scoped. |
Output: array of contacts with id, email, firstName, lastName, title, phone, companyId, linkedinUrl. Pairs with Create Contact.
Find Company
POST https://helmiq.net/api/integrations/zapier/searches/find-company
| Field | Type | Notes |
|---|---|---|
| domain | string | High-precision exact match (case-insensitive), preferred. |
| name | string | Contains-match fallback used only when no domain is supplied. |
Output: array of companies with id, name, domain, website. Pairs with Create Company.
Find Deal
POST https://helmiq.net/api/integrations/zapier/searches/find-deal
| Field | Type | Notes |
|---|---|---|
| externalId | string | High-precision exact match, preferred. |
| name | string | Contains-match against deal name and codename, fallback when no externalId. |
Output: array of deals with id, name, codeName, stage, value. Pairs with Create Deal.
Error handling
| Field | Type | Notes |
|---|---|---|
| 200 | success | Action/trigger/search succeeded. For idempotent re-runs, the body carries created: false (and duplicate: true for notes). |
| 400 | invalid_payload | The JSON body failed validation. The body includes an issues array describing each problem. |
| 401 | unauthorized | Missing, malformed, or expired Bearer token. Zapier refreshes the access token and retries automatically. |
| 403 | flag_disabled | The connected org has not enabled this surface (zapierActionsEnabled / zapierTriggersEnabled). An admin enables it in HelmIQ settings. |
| 404 | not_found | A referenced record (contact, company, deal) does not exist in the connected org. The body names which reference failed. |
| 409 | duplicate_email | Edit Contact only: the new email is already used by another contact in the org; existingContactId points at it. |
| 422 | rejected | Create Contact only: the payload passed schema validation but failed a business-rule gate; reason names the rejection. |
| 429 | rate_limited | Too many requests. See rate limits below. The response carries a Retry-After header. |
| 500 | server error | An unexpected write failure. Safe to retry; creates are idempotent. |
| 503 | disabled | The platform Zapier killswitch is engaged. Transient; retry later. |
Error bodies are JSON of the form { ok: false, error: "<code>", ... }. On a 401, Zapier transparently exchanges the refresh token for a new access token (refresh tokens are single-use and rotate on each refresh) and replays the request.
Rate limits
Limits are enforced per organization, per operation. Write actions are limited to 120 requests per minute (about 2 per second sustained). Read operations (triggers and searches) are limited to 600 requests per minute (about 10 per second sustained). Exceeding a limit returns 429 with a Retry-After header (seconds) plus X-RateLimit-Limit and X-RateLimit-Reset. Honor Retry-After before retrying.
Data isolation
Every request is scoped to the connected user's active organization. The organization and acting user are resolved from the access token on the server; they are never read from a request body or query parameter. A connection can never read or write another organization's contacts, companies, deals, tasks, or notes.
Contact
Questions about the integration: jack@helmiq.net.