Keeps your contacts in sync across Google, Microsoft, Apple and CardDAV, and cleans up duplicates.
Maintained bycontactz.app(verified)Also a maintainer?
Call this server's tools from your browser. Nothing to install.
Your ContactZapp API key. Make one in Settings, under Advanced. Start the value with Bearer.
Requests go through mcptry because browsers can't call most MCP servers directly. Header values are sent to the server for that request only and are never stored or logged.
Tool list from our last check. Connect live to refresh it.
Creates a contact. It is treated like a contact that arrives from a sync. The contact is written to one of the user's two-way accounts, which ContactZapp chooses: the earliest-linked account that keeps an address book and is not paused. The sync then copies it to every other two-way account, and never to a read-only one. It is matched like any incoming contact: - when the user's auto-merge level covers the match, the values are added to the contact the user already has (200, `merged`); - otherwise the contact is created (201, `created`), and a possible match waits in Review duplicates (201, `needs_review`). A create is never refused for the plan's contact limit. Over the limit, the contact is created frozen (201), and `contact.frozen` in the answer is true. ContactZapp keeps a frozen contact but does not sync it until the user upgrades or frees a place. Send an `Idempotency-Key` header to make a retry safe. A repeat with the same key and the same body within 24 hours answers with the first result and writes nothing. The same key with a different body is a 422.
Any string of 1 to 255 bytes that the caller chooses for this write, such as a UUID. A repeat with the same key and the same body within 24 hours answers with the first result and writes nothing; the same key with another body is a 422.
This tool isn't marked read-only, so it may change something on the server or in an account you connected.
Pick your client. Values in capitals, like YOUR_TOKEN, are yours to fill in.
claude mcp add --transport http contactzapp https://app.contactz.app/api/v1/mcp \ --header 'Authorization: Bearer YOUR_TOKEN'
Headers
Authorization (required): Your ContactZapp API key. Make one in Settings, under Advanced. Start the value with Bearer.Creates a contact. It is treated like a contact that arrives from a sync. The contact is written to one of the user's two-way accounts, which ContactZapp chooses: the earliest-linked account that keeps an address book and is not paused. The sync then copies it to every other two-way account, and never to a read-only one. It is matched like any incoming contact: - when the user's auto-merge level covers the match, the values are added to the contact the user already has (200, `merged`); - otherwise the contact is created (201, `created`), and a possible match waits in Review duplicates (201, `needs_review`). A create is never refused for the plan's contact limit. Over the limit, the contact is created frozen (201), and `contact.frozen` in the answer is true. ContactZapp keeps a frozen contact but does not sync it until the user upgrades or frees a place. Send an `Idempotency-Key` header to make a retry safe. A repeat with the same key and the same body within 24 hours answers with the first result and writes nothing. The same key with a different body is a 422.
| Argument | Type | Description |
|---|---|---|
| body* | any | |
| Idempotency-Key | string | Any string of 1 to 255 bytes that the caller chooses for this write, such as a UUID. A repeat with the same key and the same body within 24 hours answers with the first result and writes nothing; the same key with another body is a 422. |
{
"type": "object",
"$defs": {
"PartialDate": {
"type": "object",
"required": [
"year",
"month",
"day"
],
"properties": {
"day": {
"type": "integer",
"description": "1-31, or `0` when the date states none."
},
"year": {
"type": "integer",
"description": "The Gregorian year, or `0` when the date states none."
},
"month": {
"type": "integer",
"description": "1-12, or `0` when the date states none."
}
},
"description": "A calendar date, any part of which may be missing. A part that is missing\nis `0`. A birthday with no year is `{\"year\": 0, \"month\": 3, \"day\": 15}`.\n\nNever fill in a missing year yourself: check whether the date has one, and\ndo not show `0` as a year. The API never sends `0` to mean year zero. A date\nhas no time and no time zone.\n",
"additionalProperties": false
},
"LabeledValue": {
"type": "object",
"required": [
"label",
"value"
],
"properties": {
"label": {
"type": "string"
},
"value": {
"type": "string"
},
"preferred": {
"type": "boolean",
"description": "Marks the preferred entry of its field: the phone number to call, the\naddress to write to. Missing or false means nothing has said so; it does not\nmean the entry is explicitly not preferred. If your tool does not model\npreference, leave it out of every entry, so you do not clear the user's\nchoice. Not every provider stores it, so do not expect it to carry over\neverywhere.\n"
}
},
"description": "A single value carrying the source provider's own label, where one\nexists (e.g. \"Mobile\", \"Other\", vCard TYPE=CELL). `label` is empty\nstring when the source has none.\n",
"additionalProperties": false
},
"V1ContactInput": {
"allOf": [
{
"$ref": "#/$defs/V1ContactFields"
}
],
"description": "The body of a contact create or update: the contact fields a read shows\nthat a write can change, under the same names and in the same shapes, and\nnothing else.\n\nA field the body carries that is not one of them is refused with a 400\n`unknown_field`, and nothing is written. That includes the read-only `id`,\n`publicId`, `displayName` and `updatedAt`, so writing back a whole contact\nas a read returned it answers 400 `unknown_field`: send only the fields you\nchange.\n\nA create writes the fields the body carries. An update is change-only: a\nfield the body does not carry keeps its value, a field it carries empty\n(`\"\"`, `[]`, or a date whose year, month and day are all `0`) is cleared,\nand a list it carries replaces the stored list. `\"birthday\": null` clears\nthe birthday too, and a birthday the body does not carry is kept. Fields ContactZapp keeps\nthat a read does not show are never changed by an update.\n"
},
"V1ContactFields": {
"type": "object",
"properties": {
"urls": {
"type": "array",
"items": {
"$ref": "#/$defs/LabeledValue"
}
},
"notes": {
"type": "string"
},
"emails": {
"type": "array",
"items": {
"$ref": "#/$defs/LabeledValue"
}
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"phones": {
"type": "array",
"items": {
"$ref": "#/$defs/LabeledValue"
}
},
"prefix": {
"type": "string"
},
"suffix": {
"type": "string"
},
"birthday": {
"anyOf": [
{
"$ref": "#/$defs/PartialDate"
},
{
"type": "null"
}
],
"description": "Null when the contact has none. In a write, null clears the stored\nbirthday, and so does a date whose year, month and day are all\n`0`; an absent field keeps it.\n"
},
"jobTitle": {
"type": "string"
},
"lastName": {
"type": "string"
},
"nickname": {
"type": "string"
},
"addresses": {
"type": "array",
"items": {
"$ref": "#/$defs/LabeledValue"
}
},
"firstName": {
"type": "string"
},
"middleName": {
"type": "string"
},
"organization": {
"type": "string"
}
},
"description": "The contact fields a read shows and a write can change. A write takes\nexactly these names, in the same shapes a read returns them.\n"
}
},
"required": [
"body"
],
"properties": {
"body": {
"$ref": "#/$defs/V1ContactInput"
},
"Idempotency-Key": {
"type": "string",
"description": "Any string of 1 to 255 bytes that the caller chooses for this\nwrite, such as a UUID. A repeat\nwith the same key and the same body within 24 hours answers with the first result and\nwrites nothing; the same key with another body is a 422."
}
},
"additionalProperties": false
}Subscribes a URL to one event. Zapier, Make and n8n call this when an automation is switched on. The URL must use HTTPS and reach a public address. The answer is 201 with the subscription and its signing `secret`, which is shown only here. Needs the `webhooks:manage` and `contacts:read` scopes. A user can have up to 25 subscriptions. Each event is sent to the URL as a POST with a JSON body. The body is one item, exactly as the polling trigger for the same event lists it. Every delivery has these headers: - `ContactZapp-Signature`: `t=<unix seconds>,v1=<signature>`, where the signature is the hex HMAC-SHA256 of the timestamp, a full stop and the raw body, keyed with the secret. Check it, and refuse a timestamp more than five minutes from your clock. - `ContactZapp-Delivery`: the delivery's id. A retry sends the same id. - `ContactZapp-Event`: the event, for example `contact.created`. Answer with any 2xx status. An answer of 410 removes the subscription. Any other answer, or none within 10 seconds, is retried with backoff for about a day; after the last attempt the subscription is turned off. Each event is sent at least once: deduplicate by the item's `id`.
| Argument | Type | Description |
|---|---|---|
| body* | any |
{
"type": "object",
"$defs": {
"V1TriggerEvent": {
"enum": [
"contact.created",
"contact.updated",
"contacts.merged",
"match.needs_review",
"account.needs_attention"
],
"type": "string",
"description": "The events the API reports, as polling triggers.\n\n- `contact.created`: a contact was added.\n- `contact.updated`: a contact was changed.\n- `contacts.merged`: contacts were merged into one.\n- `match.needs_review`: a contact is flagged as a possible duplicate and\n waits in Review duplicates.\n- `account.needs_attention`: a linked account needs the user to look at it.\n"
},
"V1WebhookCreateRequest": {
"type": "object",
"required": [
"url",
"event"
],
"properties": {
"url": {
"type": "string",
"maxLength": 2048,
"description": "The HTTPS URL to deliver to."
},
"event": {
"$ref": "#/$defs/V1TriggerEvent"
}
},
"additionalProperties": false
}
},
"required": [
"body"
],
"properties": {
"body": {
"$ref": "#/$defs/V1WebhookCreateRequest"
}
},
"additionalProperties": false
}Returns one contact. `id` is the contact's `id` or its `publicId` (`CS-0042`). A deleted or merged contact is a 404, and so is another user's contact.
| Argument | Type | Description |
|---|---|---|
| id* | string |
{
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
}
},
"additionalProperties": false
}Checks an API key. Returns the user the key acts for, the user's plan and the key's scopes. Any live key may call it, so it is the call to make when you want to test a key. Send the key in every request as `Authorization: Bearer <key>`. Each operation names the scope it needs; a key without that scope gets 403. There is a limit on requests; past it the answer is 429 with a `Retry-After` header. Every error body has a stable `code`.
No arguments.
{
"type": "object",
"properties": {},
"additionalProperties": false
}Lists the user's linked accounts, as the Settings screen does: provider, name, sync direction, connection state and when each account last synced.
No arguments.
{
"type": "object",
"properties": {},
"additionalProperties": false
}Finds the user's contacts, or lists them. Give at most one of `email`, `phone` and `query`: - `email` and `phone` find the contacts that hold that address or number. An email is compared ignoring case, and a phone number ignoring formatting. - `query` is the same free-text search the Contacts screen has. - With none of them, every contact is listed, newest first. Pages work with a cursor: pass `nextCursor` back as `cursor`, with the same filter, until a page comes back without one. Deleted and merged contacts are never returned.
| Argument | Type | Description |
|---|---|---|
| string | ||
| limit | integer | Page size; 50 when omitted. |
| phone | string | |
| query | string | |
| cursor | string | The previous page's `nextCursor`. |
{
"type": "object",
"properties": {
"email": {
"type": "string"
},
"limit": {
"type": "integer",
"maximum": 200,
"minimum": 1,
"description": "Page size; 50 when omitted."
},
"phone": {
"type": "string"
},
"query": {
"type": "string"
},
"cursor": {
"type": "string",
"description": "The previous page's `nextCursor`."
}
},
"additionalProperties": false
}Polling trigger: returns the newest events of one type first. This is what Zapier, Make and n8n poll to start an automation. Each item has an `id` that never changes. Polling again with nothing new returns the same ids, and a new event appears at the top. Deduplicate by `id`. `contact.created`, `contact.updated`, `contacts.merged` and `account.needs_attention` come from the change history ContactZapp records. One item is one recorded event, and it is kept as long as the History screen keeps it. `match.needs_review` comes from the duplicate flags the Contacts screen shows: one item for each contact flagged as a possible duplicate and not yet merged. Its `id` changes when the flag is raised again. Each item carries what an automation needs without a second call: the contact's fields (null when the contact has since been deleted or merged), for a merge the ids of the contacts it absorbed, and for an account the account and why it needs attention.
| Argument | Type | Description |
|---|---|---|
| event* | any | |
| limit | integer | Page size; 50 when omitted. |
| cursor | string | The previous page's `nextCursor`, to read further back. |
{
"type": "object",
"$defs": {
"V1TriggerEvent": {
"enum": [
"contact.created",
"contact.updated",
"contacts.merged",
"match.needs_review",
"account.needs_attention"
],
"type": "string",
"description": "The events the API reports, as polling triggers.\n\n- `contact.created`: a contact was added.\n- `contact.updated`: a contact was changed.\n- `contacts.merged`: contacts were merged into one.\n- `match.needs_review`: a contact is flagged as a possible duplicate and\n waits in Review duplicates.\n- `account.needs_attention`: a linked account needs the user to look at it.\n"
}
},
"required": [
"event"
],
"properties": {
"event": {
"$ref": "#/$defs/V1TriggerEvent"
},
"limit": {
"type": "integer",
"maximum": 100,
"minimum": 1,
"description": "Page size; 50 when omitted."
},
"cursor": {
"type": "string",
"description": "The previous page's `nextCursor`, to read further back."
}
},
"additionalProperties": false
}Lists the webhook subscriptions of this API key's user, oldest first: the URL, the event and whether it is `active` or `disabled`. The signing secret is never shown again after the subscription is made. Needs the `webhooks:manage` scope.
No arguments.
{
"type": "object",
"properties": {},
"additionalProperties": false
}Starts a sync, the same as pressing Sync now in ContactZapp. The run shows in History as a manual run. The answer is 202 with the state of the sync: `queued` or `running`. If a sync is already going, you get that one back and no second one starts. Needs the `sync:trigger` scope.
No arguments.
{
"type": "object",
"properties": {},
"additionalProperties": false
}Changes a contact's fields, as the app's edit does. Only the fields in the body change; a field the body leaves out keeps its value. The change is recorded in History as the user's, naming the API key, and written to every two-way account the contact is in, never to a read-only one. A contact waiting in Review duplicates cannot be changed until the review is decided (409, `contact_pending_review`). `Idempotency-Key` works as it does when you create a contact.
| Argument | Type | Description |
|---|---|---|
| id* | string | The contact's `id` or its `publicId` (`CS-0042`). |
| body* | any | |
| Idempotency-Key | string | Any string of 1 to 255 bytes that the caller chooses for this write, such as a UUID. A repeat with the same key and the same body within 24 hours answers with the first result and writes nothing; the same key with another body is a 422. |
{
"type": "object",
"$defs": {
"PartialDate": {
"type": "object",
"required": [
"year",
"month",
"day"
],
"properties": {
"day": {
"type": "integer",
"description": "1-31, or `0` when the date states none."
},
"year": {
"type": "integer",
"description": "The Gregorian year, or `0` when the date states none."
},
"month": {
"type": "integer",
"description": "1-12, or `0` when the date states none."
}
},
"description": "A calendar date, any part of which may be missing. A part that is missing\nis `0`. A birthday with no year is `{\"year\": 0, \"month\": 3, \"day\": 15}`.\n\nNever fill in a missing year yourself: check whether the date has one, and\ndo not show `0` as a year. The API never sends `0` to mean year zero. A date\nhas no time and no time zone.\n",
"additionalProperties": false
},
"LabeledValue": {
"type": "object",
"required": [
"label",
"value"
],
"properties": {
"label": {
"type": "string"
},
"value": {
"type": "string"
},
"preferred": {
"type": "boolean",
"description": "Marks the preferred entry of its field: the phone number to call, the\naddress to write to. Missing or false means nothing has said so; it does not\nmean the entry is explicitly not preferred. If your tool does not model\npreference, leave it out of every entry, so you do not clear the user's\nchoice. Not every provider stores it, so do not expect it to carry over\neverywhere.\n"
}
},
"description": "A single value carrying the source provider's own label, where one\nexists (e.g. \"Mobile\", \"Other\", vCard TYPE=CELL). `label` is empty\nstring when the source has none.\n",
"additionalProperties": false
},
"V1ContactInput": {
"allOf": [
{
"$ref": "#/$defs/V1ContactFields"
}
],
"description": "The body of a contact create or update: the contact fields a read shows\nthat a write can change, under the same names and in the same shapes, and\nnothing else.\n\nA field the body carries that is not one of them is refused with a 400\n`unknown_field`, and nothing is written. That includes the read-only `id`,\n`publicId`, `displayName` and `updatedAt`, so writing back a whole contact\nas a read returned it answers 400 `unknown_field`: send only the fields you\nchange.\n\nA create writes the fields the body carries. An update is change-only: a\nfield the body does not carry keeps its value, a field it carries empty\n(`\"\"`, `[]`, or a date whose year, month and day are all `0`) is cleared,\nand a list it carries replaces the stored list. `\"birthday\": null` clears\nthe birthday too, and a birthday the body does not carry is kept. Fields ContactZapp keeps\nthat a read does not show are never changed by an update.\n"
},
"V1ContactFields": {
"type": "object",
"properties": {
"urls": {
"type": "array",
"items": {
"$ref": "#/$defs/LabeledValue"
}
},
"notes": {
"type": "string"
},
"emails": {
"type": "array",
"items": {
"$ref": "#/$defs/LabeledValue"
}
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"phones": {
"type": "array",
"items": {
"$ref": "#/$defs/LabeledValue"
}
},
"prefix": {
"type": "string"
},
"suffix": {
"type": "string"
},
"birthday": {
"anyOf": [
{
"$ref": "#/$defs/PartialDate"
},
{
"type": "null"
}
],
"description": "Null when the contact has none. In a write, null clears the stored\nbirthday, and so does a date whose year, month and day are all\n`0`; an absent field keeps it.\n"
},
"jobTitle": {
"type": "string"
},
"lastName": {
"type": "string"
},
"nickname": {
"type": "string"
},
"addresses": {
"type": "array",
"items": {
"$ref": "#/$defs/LabeledValue"
}
},
"firstName": {
"type": "string"
},
"middleName": {
"type": "string"
},
"organization": {
"type": "string"
}
},
"description": "The contact fields a read shows and a write can change. A write takes\nexactly these names, in the same shapes a read returns them.\n"
}
},
"required": [
"body",
"id"
],
"properties": {
"id": {
"type": "string",
"description": "The contact's `id` or its `publicId` (`CS-0042`)."
},
"body": {
"$ref": "#/$defs/V1ContactInput"
},
"Idempotency-Key": {
"type": "string",
"description": "Any string of 1 to 255 bytes that the caller chooses for this\nwrite, such as a UUID. A repeat\nwith the same key and the same body within 24 hours answers with the first result and\nwrites nothing; the same key with another body is a 422."
}
},
"additionalProperties": false
}We read every tool's name, description and inputs the way the model will, looking for instructions hidden from you. A clean result means no warning signs, not a guarantee. Scanned Nov 3, 2026.