Clientforce Zapier Integration

How the Clientforce Zapier integration works under the hood: the OAuth authorization flow that connects an account, the GraphQL endpoint the app reads and writes through, the events that can start a Zap, and the signed webhooks that deliver them.

Base URLhttps://api.clientforce.io

Overview

This guide documents the Clientforce Zapier integration: the interface Zapier uses to read and write the data behind your workspace (contacts, lists, campaigns, proposals, meetings and purchases) so your Zaps can act on it. It is a reference for how that integration works, not a general-purpose developer platform.

All requests go to the API host below. Every endpoint is served over HTTPS, and every request must be authenticated with an OAuth2 access token.

Base URL
https://api.clientforce.io

There are two surfaces the integration relies on most: the GraphQL endpoint at /graphql/zapier for querying and mutating data, and webhooks, which push events to a subscribed URL as they happen.

Authentication

Clientforce uses the OAuth2 authorization code flow. The Zapier app sends the user to a Clientforce consent screen in their browser; they choose a workspace and grant access; Clientforce redirects them back to Zapier with a short-lived code, which Zapier exchanges for an access token server-side.

This is a redirect-based flow, so it always involves a real user in a browser. There is no way to mint a token from client credentials alone.

The /oauth/zapier consent screen described below exists specifically for the Clientforce–Zapier integration. It is not a self-service developer registration system: there is currently no way for an arbitrary third party to register its own client_id. The client_id used throughout this section is the one issued for the Zapier integration.

1

Zapier sends the user to the authorize URL

The user's browser is redirected to the authorization endpoint with the client_id issued for the integration, the redirect_uri registered against it, and a state value generated for the attempt. All three are required. response_type is not read from the query string, because the authorization code response type is implied and fixed server-side.

Authorize URL
https://app.clientforce.io/oauth/zapier
  ?client_id=CLIENT_ID
  &redirect_uri=REGISTERED_REDIRECT_URI
  &state=RANDOM_STATE_STRING

The state value is held in the user's session and checked when they come back. It is what protects the flow against cross-site request forgery.

2

The user logs in and picks a workspace

On the consent screen the user signs in if they are not already, then selects which of their workspaces to connect. They can grant access or deny it. The workspace chosen here is the one the resulting tokens will act on.

3

Clientforce redirects back to Zapier

If the user grants access, their browser is sent to the registered redirect_uri with an authorization code and the same state that was sent in step 1:

Redirect (granted)
REGISTERED_REDIRECT_URI?code=AUTHORIZATION_CODE&state=RANDOM_STATE_STRING

If they deny access, the redirect carries an error instead of a code:

Redirect (denied)
REGISTERED_REDIRECT_URI?error=access_denied&state=RANDOM_STATE_STRING
4

Exchange the code for an access token

Server-side, the code is POSTed to the token endpoint along with the integration's client credentials and the same redirect_uri used in step 1:

cURL
curl -X POST https://api.clientforce.io/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "CLIENT_ID",
    "client_secret": "CLIENT_SECRET",
    "redirect_uri": "REGISTERED_REDIRECT_URI",
    "code": "AUTHORIZATION_CODE"
  }'

The response contains both tokens and the access token's lifetime:

JSON
{
  "token_type": "Bearer",
  "expires_in": 1296000,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
  "refresh_token": "def50200a1b2c3..."
}

Using the token

Send the access token in the Authorization header of every API call:

HTTP
Authorization: Bearer YOUR_ACCESS_TOKEN

Token lifetimes and refreshing

Access tokens are valid for 15 days and refresh tokens for 30 days. Before an access token expires, exchange the refresh token for a new pair at the same /oauth/token endpoint:

cURL
curl -X POST https://api.clientforce.io/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "client_id": "CLIENT_ID",
    "client_secret": "CLIENT_SECRET",
    "refresh_token": "REFRESH_TOKEN"
  }'

If the refresh token expires too, the user has to go through the authorization flow again from step 1.

There are no OAuth scopes. Access is not narrowed by scope. A token can do anything the connected workspace allows, so no scope parameter is sent. The client secret behaves like a password for the integration: it stays server-side and never appears in browser or mobile code.

Workspace Header

A user can belong to more than one workspace, so the access token alone does not say which workspace a request is about. Every GraphQL request must therefore carry an X-Workspace-Id header, spelled exactly like that, alongside the Authorization header. Requests without it will not resolve to a workspace.

HTTP
Authorization: Bearer YOUR_ACCESS_TOKEN
X-Workspace-Id: YOUR_WORKSPACE_ID

Finding your workspace ID

Use the listWorkspaces query to list every workspace the authenticated user belongs to. It returns the id, name, slug and the user's role for each one:

GraphQL
query ListWorkspaces {
  listWorkspaces {
    id
    name
    slug
    role
  }
}

Two different fields, two different types. The workspace_id argument used by mutations and carried in webhook payloads is type Int, not ID!. The id field on the Workspace type is type ID, which GraphQL serialises as a string. They refer to the same workspace but they are different fields on different types, so do not assume they come back in the same format. Coerce deliberately when you pass one into the other.

GraphQL API

The GraphQL endpoint lives at /graphql/zapier. Unlike a REST API, GraphQL exposes a single endpoint that accepts a query describing exactly the data you want. You ask for the fields you need and the response mirrors the shape of your query, nothing more and nothing less.

Every request is a POST whose body contains a query string and an optional variables object. Queries read data; mutations write it. Both required headers, Authorization and X-Workspace-Id, must be present on every call.

Here is a complete query that takes no arguments and returns the contacts created in the workspace:

GraphQL
query ContactCreatedList {
  contactCreatedList {
    id
    email
    first_name
    last_name
  }
}

Sent as an HTTP request:

cURL
curl -X POST https://api.clientforce.io/graphql/zapier \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-Workspace-Id: YOUR_WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query ContactCreatedList { contactCreatedList { id email first_name last_name } }"
  }'

The response comes back under a data key, keyed by the fields you asked for:

JSON
{
  "data": {
    "contactCreatedList": [
      { "id": 1, "email": "john@example.com", "first_name": "John", "last_name": "Doe" },
      { "id": 2, "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
    ]
  }
}

Note that id comes back as an unquoted number: like workspace_id, it is a GraphQL Int, not an ID.

If something goes wrong, GraphQL still returns HTTP 200 and reports the problem in an errors array, so check for errors rather than relying on the status code alone.

Zapier Integration

The Clientforce Zapier app connects your workspace to thousands of other tools without writing any code. It is built on the same API documented here, so anything you can do in a Zap you can also do directly.

A Zap is made of a trigger, an event in Clientforce that starts the automation, and one or more actions, which are the things that happen next. Clientforce provides both: 14 triggers that fire on activity in your workspace, and 7 actions that let other apps write into it.

Connect your account once in Zapier, using the same authorization code flow described above and the workspace you pick on the consent screen. The triggers and actions below then become available to any Zap.

14

Triggers

Events in Clientforce that can start a Zap: new contacts, replies, meetings, calls and more.

7

Actions

Operations other apps can perform in Clientforce: tagging, list and sequence membership, saving leads and recording purchases.

Triggers

Each trigger corresponds to an event name and delivers a payload with the fields listed below. The same event names are used for webhook deliveries, so a Zap trigger and a webhook subscription receive the same data. Select a trigger to see its payload fields.

Actions

Actions let another app write into your Clientforce workspace. Contacts are matched by email address.

Add a Tag

Attaches a tag to a contact, matched by email address. If the tag does not exist yet, it is created.

Add Subscriber to List

Adds a contact to a specified list. If no contact exists for the email address, one is created first.

Remove Subscriber from List

Removes a contact from a specified list. The contact must already exist. This action never creates one.

Add Subscriber to Sequence/Agent

Adds a contact to a specified campaign or sequence. If no contact exists for the email address, one is created first.

Remove Subscriber from Sequence/Agent

Removes a contact from a specified campaign or sequence. The contact must already exist. This action never creates one.

Save Lead

Creates a contact, or updates it if one already exists for the email address. Optionally attaches the contact to a list, a sequence, or both.

Record a Purchase

Logs a purchase event against a contact, creating the contact if one does not already exist. Purchases are deduplicated by external order ID, so re-sending the same order will not record it twice.

Webhooks

Webhooks push events to your server the moment they happen, so you do not have to poll the API. Register an HTTPS endpoint, subscribe it to the events you care about, and Clientforce will POST a JSON payload to it each time one of those events fires.

The events you can subscribe to are the same ones listed under Triggers.

Payload structure

Every delivery has the same envelope: a unique id for the delivery, the event name, and a data object holding that event's payload fields.

JSON
{
  "id": "uuid-here",
  "event": "contact.created",
  "data": {
    "workspace_id": 123456,
    "lead_id": 789,
    "email": "john@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "phone": "+15551234567",
    "timestamp": "2026-01-01T00:00:00.000Z"
  }
}

Verifying the signature

Every delivery is signed so you can confirm it genuinely came from Clientforce and was not tampered with in transit. The signature arrives in a header named Signature, and is an HMAC-SHA256 of the raw JSON request body, meaning the full envelope of id, event and data together, keyed with your endpoint secret.

To verify a delivery, recompute the HMAC over the raw, unparsed body and compare it to the header value with a constant-time comparison. Reject the request if they do not match, and never trust a payload you have not verified.

PHP
$payload   = file_get_contents('php://input'); // raw body — do not re-encode
$signature = $_SERVER['HTTP_SIGNATURE'] ?? '';
$expected  = hash_hmac('sha256', $payload, $endpointSecret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($payload, true); // safe to parse now

Sign the bytes you received. Parsing the body and re-serialising it will change the JSON, key order and whitespace included, and the signature will no longer match. Always hash the raw request body exactly as it arrived.

Responding to a webhook

Return a 2xx status as soon as you have accepted the payload, and do the real work asynchronously. A non-2xx response or a timeout is treated as a failed delivery. Deliveries can be retried, so handle events idempotently. Use the delivery id to recognise one you have already processed.

Rate Limits & Support

Rate limits

Rate limits are not currently published. If you are planning a high-volume or latency-sensitive integration, contact support for current guidance before you build against an assumed limit. As a matter of good practice, back off and retry on failures rather than retrying immediately in a tight loop.

Support

Questions about the API, your workspace connection, or a specific Zap? Our help centre is the fastest way to reach us.

Visit the Help Centre