> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# Zapier

The Jelliu app for Zapier turns events in your workspace into Zap triggers: a call ends, a contact converts, a campaign finishes. Zapier subscribes to the event, and Jelliu `POST`s each occurrence to Zapier as it happens.

The app works in one direction only: **Jelliu tells Zapier what happened.** It has triggers and no actions. If you want your agents to *act* inside the apps you have connected to Zapier (create a CRM deal, add a spreadsheet row) during a conversation, connect **Zapier MCP** to your agents instead. See [Hosted automation providers](/platform/integrations#hosted-automation-providers).

## How it works

```mermaid
sequenceDiagram
    participant Owner as Workspace owner
    participant Jelliu
    participant Zapier
    Owner->>Jelliu: Generate a Zapier API key (dashboard)
    Jelliu-->>Owner: zk_... (shown once)
    Owner->>Zapier: Connect the Jelliu account with the zk_ key
    Zapier->>Jelliu: GET /zapier/triggers/{event} (sample data)
    Zapier->>Jelliu: POST /zapier/subscribe (Zap turned on)
    Jelliu-->>Zapier: 201 { data: { id, event, target_url } }
    Note over Jelliu: A call ends, a contact converts...
    Jelliu->>Zapier: POST target_url { event, timestamp, data }
    Zapier->>Jelliu: DELETE /zapier/subscribe/{id} (Zap turned off)
```

## The Zapier API key

Zapier authenticates with its own credential, separate from workspace API keys:

|         | Zapier API key                                                       | Workspace API key                           |
| ------- | -------------------------------------------------------------------- | ------------------------------------------- |
| Format  | `zk_` followed by 64 hexadecimal characters (67 characters in total) | `jl_` followed by 64 hexadecimal characters |
| Header  | `x-zapier-api-key: zk_...`                                           | `Authorization: Bearer jl_...`              |
| Reaches | Only the `/zapier/*` endpoints below                                 | The REST API under `/api/*`                 |

A `zk_` key reads call and contact data out of your workspace and streams it to any URL it subscribes. Treat it like an admin credential: store it only in Zapier, and revoke it when you stop using the integration.

### Generate a key

In the dashboard, open **Integrations → Zapier → Generate API Key**. The key is shown **once**. Jelliu stores only a keyed hash of it, so a lost key cannot be recovered; generate a new one.

Generating and revoking Zapier keys is restricted to the workspace **owner**, in a signed-in dashboard session. The underlying endpoints (`POST /api/zapier/api-key` and `DELETE /api/zapier/api-key/{id}`) refuse workspace API keys, even `full` ones, with `403 FORBIDDEN`:

`This action requires a signed-in admin session — API keys cannot perform credential-management operations.`

That way a leaked `jl_` key cannot mint a long-lived `zk_` credential that would survive revoking it.

### Revoke a key

Revoke the key in the same dashboard screen. From then on, requests carrying it receive `401`.

Revoking a key does **not** delete the subscriptions it created. Active Zaps keep receiving events, but Zapier can no longer authenticate to unsubscribe them. **Turn off your Zaps first, then revoke the key.**

## Connect Jelliu in Zapier

#### Pick a trigger

In the Zap editor, search for **Jelliu** and choose one of the [triggers](#triggers), for example **Call Completed**.

#### Connect your account

Paste the `zk_` key. Zapier checks it with `GET /zapier/triggers/contact.created?limit=1`, which returns `200` for a valid key and `401` otherwise.

#### Load sample data

Zapier calls `GET /zapier/triggers/{event}?limit=3` to show recent records from your workspace while you map fields. See [Sample data](#sample-data) for why these records look different from live events.

#### Turn the Zap on

Zapier calls `POST /zapier/subscribe`. From that moment, every matching event is delivered to the Zap.

## Triggers

| Zapier trigger             | Event                | Sent when                                                                                                                    | `data` fields                                                                                                                                                                         |
| -------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Call Completed             | `call.completed`     | A call ended and was analysed, and it was neither a technical failure nor a voicemail.                                       | `callId`, `campaignId`, `contactId`, `agentId`, `phoneNumber`, `outcome`, `sentimentScore`, `duration`, `summary`                                                                     |
| Call Failed                | `call.failed`        | A call ended in a technical failure. Also sent when background reconciliation closes a call that failed or was not answered. | Same as Call Completed. Reconciled calls carry `callId`, `agentId`, `campaignId`, `contactId`, `phoneNumber`, `outcome` (`failed` or `no_answer`), `duration` and `reconciled: true`. |
| Voicemail Detected         | `call.voicemail`     | A call's outcome is `voicemail`.                                                                                             | Same as Call Completed.                                                                                                                                                               |
| Call Escalated             | `call.escalated`     | A call was transferred to a human and the transfer completed.                                                                | `callId`, `phoneNumber`, `transferredTo`                                                                                                                                              |
| Campaign Completed         | `campaign.completed` | A campaign finished working through all of its contacts.                                                                     | `campaignId`                                                                                                                                                                          |
| Contact Converted          | `contact.converted`  | A contact converted, on a call or in a text conversation.                                                                    | From a call: `contactId`, `callId`, `campaignId`. From a conversation: `contactId`, `callId: null`, `conversationId`, `campaignId`, `source: "conversation"`.                         |
| Contact Marked Do-Not-Call | `contact.dnc`        | A person asked not to be called again during a call.                                                                         | `contactId`, `callId`, `phoneNumber`                                                                                                                                                  |
| New Contact                | `contact.created`    | Accepted as a subscription, but **no events are delivered for it yet**. Sample data works.                                   | None yet                                                                                                                                                                              |

`duration` is in seconds. `outcome` is a string such as `sale_closed`, `callback_scheduled`, `no_answer` or `voicemail`, and can be `null` when there is no analysis. Treat every field as optional.

Zapier payloads are leaner than [outbound webhooks](/webhooks): `call.completed` here does not include `dataCollection` or `kpiData`. If you need the extracted data, subscribe a webhook to `call.completed` instead, or use both.

## Delivery

For every active subscription whose event matches, Jelliu sends:

```http
POST {target_url} HTTP/1.1
Content-Type: application/json
X-Zapier-Event: call.completed
X-Zapier-Delivery-Id: 3f1c2b9a-8d4e-4f6a-9b1c-2e3d4f5a6b7c
```

```json
{
  "event": "call.completed",
  "timestamp": "2026-09-14T15:42:07.318Z",
  "data": {
    "callId": "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10",
    "campaignId": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90",
    "contactId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
    "phoneNumber": "+573001234567",
    "outcome": "callback_scheduled",
    "sentimentScore": 0.6,
    "duration": 184,
    "summary": "El cliente pidió que lo llamen el jueves en la tarde."
  }
}
```

The Jelliu Zapier app adds an `id` to each record (the `callId`, `contactId` or `campaignId` from `data`).

|                        |                                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Success                | Any `2xx` response.                                                                                                |
| Timeout                | 10 seconds.                                                                                                        |
| Retries                | **None.** Each event is attempted once per subscription.                                                           |
| Signature              | None. Zapier deliveries are not signed.                                                                            |
| `X-Zapier-Delivery-Id` | A new random UUID for each delivery.                                                                               |
| Automatic disabling    | After **5 consecutive failed deliveries** the subscription is deactivated. A successful delivery resets the count. |

A deactivated subscription stops receiving events silently: Zapier is not told. If a Zap stops firing, check `is_active` and `last_error` with [`GET /api/zapier`](#list-subscriptions), then turn the Zap off and on again in Zapier, which creates a fresh subscription.

`contact.dnc` can be sent more than once for the same contact, because every path that hears an opt-out records it. Make the Zap's downstream step idempotent on `contactId`.

## Endpoints

The Zapier app calls these for you. You can also call them directly, for example to feed the same REST Hooks into your own automation tool.

All `/zapier/*` endpoints authenticate with `x-zapier-api-key`.

### Subscribe

`POST /zapier/subscribe`

**`event`** `string` — required

One of `call.completed`, `call.failed`, `call.voicemail`, `call.escalated`, `campaign.completed`, `contact.created`, `contact.converted`, `contact.dnc`.

---

**`target_url`** `string` — required

Where events are delivered. A valid URL of up to 2000 characters that resolves to a public address: private, internal and loopback targets are rejected.

---

**`cURL`**

```bash title="cURL"
curl -sS -X POST "https://api.jelliu.co/zapier/subscribe" \
  -H "x-zapier-api-key: $JELLIU_ZAPIER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "call.completed",
    "target_url": "https://hooks.example.com/jelliu/call-completed"
  }'
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/zapier/subscribe', {
  method: 'POST',
  headers: {
    'x-zapier-api-key': process.env.JELLIU_ZAPIER_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    event: 'call.completed',
    target_url: 'https://hooks.example.com/jelliu/call-completed',
  }),
});
const { data } = await res.json();
console.log(res.status, data.id);
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.post(
    "https://api.jelliu.co/zapier/subscribe",
    headers={"x-zapier-api-key": os.environ["JELLIU_ZAPIER_KEY"]},
    json={
        "event": "call.completed",
        "target_url": "https://hooks.example.com/jelliu/call-completed",
    },
    timeout=30,
)
print(res.status_code, res.json()["data"]["id"])
```

Response `201`:

```json
{
  "data": {
    "id": "c9a4e1f2-3b5d-4c6e-8f7a-9b0c1d2e3f4a",
    "event": "call.completed",
    "target_url": "https://hooks.example.com/jelliu/call-completed"
  }
}
```

Keep `data.id`: it is what you pass to unsubscribe.

### Unsubscribe

`DELETE /zapier/subscribe/{id}`

**`cURL`**

```bash title="cURL"
curl -sS -X DELETE "https://api.jelliu.co/zapier/subscribe/c9a4e1f2-3b5d-4c6e-8f7a-9b0c1d2e3f4a" \
  -H "x-zapier-api-key: $JELLIU_ZAPIER_KEY"
```

**`Node.js`**

```javascript title="Node.js"
const id = 'c9a4e1f2-3b5d-4c6e-8f7a-9b0c1d2e3f4a';
const res = await fetch(`https://api.jelliu.co/zapier/subscribe/${id}`, {
  method: 'DELETE',
  headers: { 'x-zapier-api-key': process.env.JELLIU_ZAPIER_KEY },
});
console.log(res.status, await res.json());
```

**`Python`**

```python title="Python"
import os
import requests

sub_id = "c9a4e1f2-3b5d-4c6e-8f7a-9b0c1d2e3f4a"
res = requests.delete(
    f"https://api.jelliu.co/zapier/subscribe/{sub_id}",
    headers={"x-zapier-api-key": os.environ["JELLIU_ZAPIER_KEY"]},
    timeout=30,
)
print(res.status_code, res.json())
```

Response `200`:

```json
{ "data": { "removed": true } }
```

An unknown or already removed id returns `404` with code `SUBSCRIPTION_NOT_FOUND` and the message `Zapier subscription not found`.

### Sample data

`GET /zapier/triggers/{event}?limit=10`

Returns the newest records for the event as a **flat JSON array, without a `data` wrapper**, which is the shape Zapier expects. `limit` accepts 1 to 100 and defaults to 10. A `page` parameter is accepted and ignored.

**`cURL`**

```bash title="cURL"
curl -sS "https://api.jelliu.co/zapier/triggers/call.completed?limit=3" \
  -H "x-zapier-api-key: $JELLIU_ZAPIER_KEY"
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/zapier/triggers/call.completed?limit=3', {
  headers: { 'x-zapier-api-key': process.env.JELLIU_ZAPIER_KEY },
});
console.log(await res.json());
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.get(
    "https://api.jelliu.co/zapier/triggers/call.completed",
    headers={"x-zapier-api-key": os.environ["JELLIU_ZAPIER_KEY"]},
    params={"limit": 3},
    timeout=30,
)
print(res.json())
```

```json
[
  {
    "id": "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10",
    "status": "completed",
    "outcome": "callback_scheduled",
    "duration_seconds": 184,
    "call_direction": "outbound",
    "created_at": "2026-09-14T15:39:03.120Z",
    "campaign_id": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90"
  }
]
```

Sample records are **database rows**, not event payloads. Call triggers return `id`, `status`, `outcome`, `duration_seconds`, `call_direction`, `created_at` and `campaign_id`; campaign and contact triggers return the campaign or contact row. Live events arrive as `{ event, timestamp, data }` with the camelCase fields listed in [Triggers](#triggers). Map your Zap's fields from a live event, not from the sample.

How samples are selected:

| Event                              | Newest records where                     |
| ---------------------------------- | ---------------------------------------- |
| `call.completed`                   | call `status` is `completed`             |
| `call.failed`, `call.voicemail`    | call `outcome` is `failed` / `voicemail` |
| `call.escalated`                   | call `outcome` is `escalated`            |
| `campaign.completed`               | campaign `status` is `completed`         |
| `contact.converted`, `contact.dnc` | contact `status` is `converted` / `dnc`  |
| `contact.created`                  | any contact                              |

### List subscriptions

`GET /api/zapier` lists the workspace's Zapier rows. Unlike the endpoints above, it is part of the REST API: authenticate with a workspace API key (`read` is enough).

**`cURL`**

```bash title="cURL"
curl -sS "https://api.jelliu.co/api/zapier" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/api/zapier', {
  headers: { Authorization: `Bearer ${process.env.JELLIU_API_KEY}` },
});
const { data } = await res.json();
for (const sub of data.filter((s) => s.event !== '__api_key__')) {
  console.log(sub.event, sub.is_active, sub.failure_count, sub.last_error);
}
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.get(
    "https://api.jelliu.co/api/zapier",
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
for sub in res.json()["data"]:
    if sub["event"] != "__api_key__":
        print(sub["event"], sub["is_active"], sub["failure_count"], sub["last_error"])
```

```json
{
  "data": [
    {
      "id": "c9a4e1f2-3b5d-4c6e-8f7a-9b0c1d2e3f4a",
      "tenant_id": "9f0fafb4-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
      "event": "call.completed",
      "target_url": "https://hooks.zapier.com/hooks/standard/123456/abcdef/",
      "is_active": true,
      "last_delivered_at": "2026-09-14T15:42:07.512Z",
      "delivery_count": 42,
      "failure_count": 0,
      "last_error": null,
      "created_at": "2026-09-01T10:00:00.000Z",
      "updated_at": "2026-09-14T15:42:07.512Z",
      "deleted_at": null
    }
  ]
}
```

Rows whose `event` is `__api_key__` represent generated Zapier API keys, not subscriptions; their `id` is what the dashboard revokes. The key itself is never returned.

## Errors

| Status | Code                     | When                                                                                                                                                                     |
| ------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `VALIDATION_FAILED`      | Unknown `event`, invalid or disallowed `target_url`, or a malformed id.                                                                                                  |
| `401`  | `UNAUTHORIZED`           | `Missing or invalid API key` (header absent or not a 67-character `zk_` key) or `Invalid API key` (unknown or revoked).                                                  |
| `403`  | `PLAN_LIMIT_EXCEEDED`    | `Maximum 50 Zapier credentials per tenant`.                                                                                                                              |
| `403`  | `FORBIDDEN`              | The workspace has not accepted the Data Processing Agreement: `You must accept the Data Processing Agreement before using the platform. Please visit the settings page.` |
| `403`  | `TENANT_SUSPENDED`       | The workspace is suspended.                                                                                                                                              |
| `404`  | `SUBSCRIPTION_NOT_FOUND` | Unsubscribing an id that does not exist or was already removed.                                                                                                          |
| `429`  | `RATE_LIMIT_EXCEEDED`    | More than 200 requests per minute from one IP.                                                                                                                           |

## Limits

* **50 per workspace, keys and subscriptions combined.** Every active Zapier API key and every active subscription counts toward the same ceiling of 50. Remove Zaps you no longer use.
* **Rate limit.** `/zapier/*` accepts 200 requests per minute per client IP.
* **No plan gate.** The Zapier triggers are available on every plan.

## Related

#### [Webhooks](/webhooks)

Signed, retried deliveries with richer payloads and filters.

#### [Integrations](/platform/integrations)

Let agents act in your apps through Zapier MCP and custom MCP servers.

#### [CRM sync with webhooks](/recipes/crm-sync-with-webhooks)

Build an idempotent receiver that writes call outcomes to your CRM.