{
  "openapi": "3.1.0",
  "info": {
    "title": "ContactZapp API",
    "version": "1.0.0",
    "description": "The ContactZapp API reads and writes the contacts in a ContactZapp\naccount, so that Zapier, Make, n8n and your own code can use them.\n\nCreate an API key in ContactZapp under Settings, Advanced. Send it with\nevery request as `Authorization: Bearer <key>`. The first call to try is\n`GET /api/v1/me`.\n"
  },
  "servers": [
    {
      "url": "https://app.contactz.app"
    }
  ],
  "paths": {
    "/api/v1/openapi.json": {
      "get": {
        "operationId": "getV1OpenAPI",
        "summary": "This API's OpenAPI document.",
        "description": "Returns this API's OpenAPI 3.1 document as JSON. It needs no API key,\nso tools such as Pipedream and Activepieces can import it from the\nlive API.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "getV1Me",
        "summary": "Who this API key acts for, on which plan, with which scopes.",
        "description": "Checks an API key. Returns the user the key acts for, the user's plan and\nthe key's scopes. Any live key may call it, so it is the call to make when\nyou want to test a key.\n\nSend the key in every request as `Authorization: Bearer <key>`. Each\noperation names the scope it needs; a key without that scope gets 403.\nThere is a limit on requests; past it the answer is 429 with a\n`Retry-After` header. Every error body has a stable `code`.\n",
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The key's user, plan and scopes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/V1Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/V1RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/V1InternalError"
          },
          "503": {
            "$ref": "#/components/responses/V1Unavailable"
          }
        }
      }
    },
    "/api/v1/contacts": {
      "get": {
        "operationId": "listV1Contacts",
        "summary": "Find the user's contacts by email, phone or text, or list them.",
        "description": "Finds the user's contacts, or lists them. Give at most one of `email`,\n`phone` and `query`:\n\n- `email` and `phone` find the contacts that hold that address or number.\n  An email is compared ignoring case, and a phone number ignoring\n  formatting.\n- `query` is the same free-text search the Contacts screen has.\n- With none of them, every contact is listed, newest first.\n\nPages work with a cursor: pass `nextCursor` back as `cursor`, with the same\nfilter, until a page comes back without one. Deleted and merged contacts are\nnever returned.\n",
        "security": [
          {
            "apiKey": [
              "contacts:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "phone",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "query",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The previous page's `nextCursor`."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            },
            "description": "Page size; 50 when omitted."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of contacts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ContactPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/V1InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/V1Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/V1Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/V1RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/V1InternalError"
          },
          "503": {
            "$ref": "#/components/responses/V1Unavailable"
          }
        }
      },
      "post": {
        "operationId": "createV1Contact",
        "summary": "Create a contact, treated like one arriving from a sync.",
        "description": "Creates a contact. It is treated like a contact that arrives from a sync.\n\nThe contact is written to one of the user's two-way accounts, which\nContactZapp chooses: the earliest-linked account that keeps an address book\nand is not paused. The sync then copies it to every other two-way account,\nand never to a read-only one. It is matched like any incoming contact:\n\n- when the user's auto-merge level covers the match, the values are added to\n  the contact the user already has (200, `merged`);\n- otherwise the contact is created (201, `created`), and a possible match\n  waits in Review duplicates (201, `needs_review`).\n\nA create is never refused for the plan's contact limit. Over the limit, the\ncontact is created frozen (201), and `contact.frozen` in the answer is true.\nContactZapp keeps a frozen contact but does not sync it until the user\nupgrades or frees a place.\n\nSend an `Idempotency-Key` header to make a retry safe. A repeat with the same\nkey and the same body within 24 hours answers with the first result and\nwrites nothing. The same key with a different body is a 422.\n",
        "security": [
          {
            "apiKey": [
              "contacts:write"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/V1IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1ContactInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The values were merged into a contact the user has (`merged`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ContactWriteResult"
                }
              }
            }
          },
          "201": {
            "description": "A new contact (`created`, or `needs_review` with its possible matches).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ContactWriteResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/V1BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/V1Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/V1Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/V1Conflict"
          },
          "413": {
            "$ref": "#/components/responses/V1BodyTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/V1Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/V1RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/V1InternalError"
          },
          "503": {
            "$ref": "#/components/responses/V1Unavailable"
          }
        }
      }
    },
    "/api/v1/contacts/{id}": {
      "get": {
        "operationId": "getV1Contact",
        "summary": "One of the user's contacts.",
        "description": "Returns one contact. `id` is the contact's `id` or its `publicId`\n(`CS-0042`). A deleted or merged contact is a 404, and so is another user's\ncontact.\n",
        "security": [
          {
            "apiKey": [
              "contacts:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The contact.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Contact"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/V1Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/V1Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/V1NotFound"
          },
          "429": {
            "$ref": "#/components/responses/V1RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/V1InternalError"
          },
          "503": {
            "$ref": "#/components/responses/V1Unavailable"
          }
        }
      },
      "patch": {
        "operationId": "updateV1Contact",
        "summary": "Change a contact's fields, as the app's edit does.",
        "description": "Changes a contact's fields, as the app's edit does. Only the fields in the\nbody change; a field the body leaves out keeps its value.\n\nThe change is recorded in History as the user's, naming the API key, and\nwritten to every two-way account the contact is in, never to a read-only\none. A contact waiting in Review duplicates cannot be changed until the\nreview is decided (409, `contact_pending_review`). `Idempotency-Key` works\nas it does when you create a contact.\n",
        "security": [
          {
            "apiKey": [
              "contacts:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact's `id` or its `publicId` (`CS-0042`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/V1IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V1ContactInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The contact as it is now (`updated`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1ContactWriteResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/V1BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/V1Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/V1Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/V1NotFound"
          },
          "409": {
            "$ref": "#/components/responses/V1Conflict"
          },
          "413": {
            "$ref": "#/components/responses/V1BodyTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/V1Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/V1RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/V1InternalError"
          },
          "503": {
            "$ref": "#/components/responses/V1Unavailable"
          }
        }
      }
    },
    "/api/v1/accounts": {
      "get": {
        "operationId": "listV1Accounts",
        "summary": "The user's linked accounts.",
        "description": "Lists the user's linked accounts, as the Settings screen does: provider,\nname, sync direction, connection state and when each account last synced.\n",
        "security": [
          {
            "apiKey": [
              "contacts:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The linked accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1AccountList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/V1Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/V1Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/V1RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/V1InternalError"
          },
          "503": {
            "$ref": "#/components/responses/V1Unavailable"
          }
        }
      }
    },
    "/api/v1/triggers/{event}": {
      "get": {
        "operationId": "listV1TriggerEvents",
        "summary": "A polling trigger — the newest events of one type first.",
        "description": "Polling trigger: returns the newest events of one type first. This is what\nZapier, Make and n8n poll to start an automation. Each item has an `id` that\nnever changes. Polling again with nothing new returns the same ids, and a\nnew event appears at the top. Deduplicate by `id`.\n\n`contact.created`, `contact.updated`, `contacts.merged` and\n`account.needs_attention` come from the change history ContactZapp records.\nOne item is one recorded event, and it is kept as long as the History screen\nkeeps it. `match.needs_review` comes from the duplicate flags the Contacts\nscreen shows: one item for each contact flagged as a possible duplicate and\nnot yet merged. Its `id` changes when the flag is raised again.\n\nEach item carries what an automation needs without a second call: the\ncontact's fields (null when the contact has since been deleted or merged),\nfor a merge the ids of the contacts it absorbed, and for an account the\naccount and why it needs attention.\n",
        "security": [
          {
            "apiKey": [
              "contacts:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "event",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/V1TriggerEvent"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The previous page's `nextCursor`, to read further back."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "Page size; 50 when omitted."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of events, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1TriggerPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/V1InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/V1Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/V1Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/V1NotFound"
          },
          "429": {
            "$ref": "#/components/responses/V1RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/V1InternalError"
          },
          "503": {
            "$ref": "#/components/responses/V1Unavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A personal API key, created in ContactZapp under Settings, Advanced and\nsent as `Authorization: Bearer cz_live_...` on `/api/v1` routes and\nnowhere else. ContactZapp keeps only a hash of the key. The array on an operation's requirement names\nthe one scope (`ApiKeyScope`) the operation needs; an empty array\nmeans any live key.\n"
      }
    },
    "parameters": {
      "V1IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "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.\n",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "V1Unauthorized": {
        "description": "No API key (`api_key_missing`; a session cookie alone counts as none),\nor one that is not a live key: unknown, revoked or malformed\n(`api_key_invalid`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1Forbidden": {
        "description": "The key lacks the scope this route needs (`insufficient_scope`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1RateLimited": {
        "description": "The user's keys together made 60 requests in the last minute\n(`rate_limited`). Retry-After says, in seconds, when the next request\nwould be served.\n",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1InvalidRequest": {
        "description": "A query parameter this route cannot use (`invalid_parameter`): a\n`limit` out of range, a `cursor` this route did not issue, or two\nfilters where one is allowed.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1NotFound": {
        "description": "No such contact or trigger for this key's user (`not_found`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1InternalError": {
        "description": "The server could not complete the request (`internal_error`). The cause is logged, never returned.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1Unavailable": {
        "description": "The key or the rate limit could not be checked (`unavailable`). Retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1BadRequest": {
        "description": "The body is not a contact (`invalid_body`), carries an\nimpossible date (`invalid_date`) or no value at all (`empty_contact`),\nor the Idempotency-Key header is empty or too long\n(`idempotency_key_invalid`), or the body carries a field a contact\nwrite does not take (`unknown_field`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1BodyTooLarge": {
        "description": "The request body is larger than the API accepts (`body_too_large`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1Conflict": {
        "description": "The contact waits in Review duplicates\n(`contact_pending_review`), was merged away or deleted\n(`contact_gone`), or changed while it was written (`contact_changed`,\nretry); or a request with this Idempotency-Key is still being handled\n(`idempotency_key_in_progress`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "V1Unprocessable": {
        "description": "The user has no two-way account that keeps an address\nbook and is not paused (`no_account_to_write_to`), or this\nIdempotency-Key was used with another body in the last 24 hours\n(`idempotency_key_reused`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      }
    },
    "schemas": {
      "PartialDate": {
        "type": "object",
        "required": [
          "year",
          "month",
          "day"
        ],
        "additionalProperties": false,
        "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",
        "properties": {
          "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."
          },
          "day": {
            "type": "integer",
            "description": "1-31, or `0` when the date states none."
          }
        }
      },
      "LabeledValue": {
        "type": "object",
        "required": [
          "label",
          "value"
        ],
        "additionalProperties": false,
        "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",
        "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"
          }
        }
      },
      "AccountProvider": {
        "type": "string",
        "enum": [
          "google",
          "microsoft",
          "apple",
          "carddav"
        ],
        "description": "The provider of a linked account: `google`, `microsoft`, `apple` (iCloud)\nor `carddav`.\n"
      },
      "AccountDisplayStatus": {
        "type": "string",
        "enum": [
          "never_synced",
          "synced",
          "error",
          "reauthorize",
          "unlinked",
          "user_paused",
          "plan_paused",
          "ok"
        ],
        "description": "How well one linked account is connected.\n\n- `never_synced`: the account is linked and has not synced yet.\n- `synced`: the last sync worked.\n- `error`: the last sync failed.\n- `reauthorize`: the user has to sign in to the account again in ContactZapp.\n- `unlinked`: the account was unlinked.\n- `user_paused`: the user paused the account.\n- `plan_paused`: the account is paused because the user's plan does not\n  cover it. A plan change lifts it.\n- `ok`: connected and healthy. `synced` is what a healthy account reads as today.\n"
      },
      "SyncDirection": {
        "type": "string",
        "enum": [
          "bidirectional",
          "read_only"
        ],
        "description": "How changes flow for one linked account. `bidirectional` is two-way sync.\n`read_only` accounts are read from and never written to.\n"
      },
      "ApiKeyScope": {
        "type": "string",
        "description": "One permission an API key carries: `contacts:read` reads contacts,\naccounts and events, and `contacts:write` creates and changes contacts.\n",
        "enum": [
          "contacts:read",
          "contacts:write"
        ]
      },
      "V1Me": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "userId",
          "plan",
          "scopes"
        ],
        "properties": {
          "userId": {
            "type": "string"
          },
          "plan": {
            "type": "string",
            "description": "The user's plan display label, as DashboardSnapshot.plan carries\nit (vault.UserPlan) — an open string, not an enum.\n"
          },
          "scopes": {
            "type": "array",
            "description": "The scopes of the key that made this request.",
            "items": {
              "$ref": "#/components/schemas/ApiKeyScope"
            }
          }
        }
      },
      "V1ContactFields": {
        "type": "object",
        "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",
        "properties": {
          "firstName": {
            "type": "string"
          },
          "middleName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "prefix": {
            "type": "string"
          },
          "suffix": {
            "type": "string"
          },
          "nickname": {
            "type": "string"
          },
          "organization": {
            "type": "string"
          },
          "jobTitle": {
            "type": "string"
          },
          "emails": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LabeledValue"
            }
          },
          "phones": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LabeledValue"
            }
          },
          "addresses": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LabeledValue"
            }
          },
          "urls": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LabeledValue"
            }
          },
          "birthday": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/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"
          },
          "notes": {
            "type": "string"
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "V1Contact": {
        "description": "One contact, as the public API shows it: the vCard-shaped fields an\nautomation reads, under names that do not change. An absent value is an\nempty string or list. `id`, `publicId`, `displayName`, `updatedAt` and `frozen` are\nread-only; every other field can be changed by a write.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/V1ContactFields"
          },
          {
            "type": "object",
            "required": [
              "id",
              "publicId",
              "displayName",
              "firstName",
              "middleName",
              "lastName",
              "prefix",
              "suffix",
              "nickname",
              "organization",
              "jobTitle",
              "emails",
              "phones",
              "addresses",
              "urls",
              "birthday",
              "notes",
              "labels",
              "updatedAt",
              "frozen"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "The contact's stable id."
              },
              "publicId": {
                "type": "string",
                "description": "The id the app shows (`CS-0042`); empty for a contact the app has not numbered yet."
              },
              "displayName": {
                "type": "string",
                "description": "The name the app shows for the contact."
              },
              "updatedAt": {
                "type": "string",
                "format": "date-time",
                "description": "When the contact last changed."
              },
              "frozen": {
                "type": "boolean",
                "description": "True when the user's plan freezes this contact: ContactZapp keeps it\nbut does not sync it to their accounts until they upgrade or free a\nplace under the plan's contact limit. A create over the limit still\nanswers 201 and returns the new contact with `frozen` true.\n"
              }
            }
          }
        ]
      },
      "V1ContactPage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "contacts"
        ],
        "properties": {
          "contacts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1Contact"
            }
          },
          "nextCursor": {
            "type": "string",
            "description": "Present when another page follows; pass it back as `cursor`."
          }
        }
      },
      "V1Account": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "provider",
          "name",
          "email",
          "direction",
          "status",
          "lastSyncAt"
        ],
        "description": "One linked account, as the public API shows it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "provider": {
            "$ref": "#/components/schemas/AccountProvider"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "direction": {
            "$ref": "#/components/schemas/SyncDirection"
          },
          "status": {
            "$ref": "#/components/schemas/AccountDisplayStatus"
          },
          "lastSyncAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the account last synced; null if it never has."
          }
        }
      },
      "V1AccountList": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "accounts"
        ],
        "properties": {
          "accounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1Account"
            }
          }
        }
      },
      "V1TriggerEvent": {
        "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",
        "enum": [
          "contact.created",
          "contact.updated",
          "contacts.merged",
          "match.needs_review",
          "account.needs_attention"
        ]
      },
      "V1AccountAttention": {
        "type": "string",
        "description": "Why an account needs attention.\n\n- `sync_failed`: reading the account failed.\n- `reconnect_needed`: the account's sign-in no longer works, and syncs skip\n  it until the user signs in again.\n- `paused_over_plan_limit`: a sync skipped the account because the user's\n  plan paused it.\n- `disconnected_after_pause`: the account stayed paused for too long and was\n  disconnected.\n",
        "enum": [
          "sync_failed",
          "reconnect_needed",
          "paused_over_plan_limit",
          "disconnected_after_pause"
        ]
      },
      "V1TriggerItem": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "event",
          "occurredAt"
        ],
        "description": "One trigger event. Which of the optional fields it carries depends on\n`event`: the contact events carry `contactId` and `contact`; a merge\nalso carries `absorbedContactIds` and `automatic`; a review carries\nthe pair — `contactId`, `contact`, `pairedContactId`, `pairedContact`\n— and `confidence`; an account event carries `accountId`, `account`\nand `attention`.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "The event's stable id. Deduplicate by it."
          },
          "event": {
            "$ref": "#/components/schemas/V1TriggerEvent"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "contactId": {
            "type": "string",
            "description": "The contact the event is about; for a merge, the contact that survived."
          },
          "contact": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/V1Contact"
              },
              {
                "type": "null"
              }
            ],
            "description": "The contact as it is now; null when it has since been deleted or merged."
          },
          "absorbedContactIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The ids of the contacts merged into `contactId`."
          },
          "automatic": {
            "type": "boolean",
            "description": "The merge was made by auto-merge rather than by the user."
          },
          "pairedContactId": {
            "type": "string",
            "description": "For a review, the other contact of the pair."
          },
          "pairedContact": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/V1Contact"
              },
              {
                "type": "null"
              }
            ],
            "description": "For a review, the other contact of the pair as it is now."
          },
          "confidence": {
            "type": "integer",
            "description": "How sure the matcher is that the two contacts are the same person, 0-100."
          },
          "accountId": {
            "type": "string",
            "description": "The account the event is about; absent when the account has since been removed."
          },
          "account": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/V1Account"
              },
              {
                "type": "null"
              }
            ],
            "description": "The account as it is now; null when it has since been removed."
          },
          "attention": {
            "$ref": "#/components/schemas/V1AccountAttention"
          }
        }
      },
      "V1TriggerPage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/V1TriggerItem"
            }
          },
          "nextCursor": {
            "type": "string",
            "description": "Present when older events follow; pass it back as `cursor`."
          }
        }
      },
      "V1ContactInput": {
        "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",
        "allOf": [
          {
            "$ref": "#/components/schemas/V1ContactFields"
          }
        ]
      },
      "V1ContactWriteOutcome": {
        "type": "string",
        "enum": [
          "created",
          "merged",
          "needs_review",
          "updated"
        ],
        "description": "What a contact write did. `created`: a new contact, which matched no\ncontact the user has. `merged`: the contact matched one the user has\nand the user's auto-merge level covers the match, so the values were\nadded to that contact; nothing was created. `needs_review`: a new\ncontact, which matched contacts the user has that their auto-merge\nlevel does not merge; the pair waits in Review duplicates.\n`updated`: an existing contact was changed.\n"
      },
      "V1ContactMatch": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "contact",
          "confidence"
        ],
        "description": "A contact the user has that a created contact matched,\nand the matcher's score for the pair.\n",
        "properties": {
          "contact": {
            "$ref": "#/components/schemas/V1Contact"
          },
          "confidence": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "The matcher's score for the pair, 0-100."
          }
        }
      },
      "V1ContactWriteResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "outcome",
          "contact"
        ],
        "description": "The answer of a contact write. `contact` is the contact\nwritten (the new one, the one merged into, or the one updated), as\nevery read shows it (V1Contact). `possibleMatches` is\npresent only for `needs_review`.\n",
        "properties": {
          "outcome": {
            "$ref": "#/components/schemas/V1ContactWriteOutcome"
          },
          "contact": {
            "$ref": "#/components/schemas/V1Contact"
          },
          "possibleMatches": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/V1ContactMatch"
            }
          }
        }
      },
      "V1ErrorCode": {
        "type": "string",
        "description": "The stable, machine-readable reason of an error. Show `message` to the\nperson building the automation, and branch on `code`.\n\n- `api_key_missing`: the request has no API key.\n- `api_key_invalid`: the API key is not valid. It may have been revoked.\n- `insufficient_scope`: the API key does not have the scope this operation needs.\n- `rate_limited`: too many requests. Wait the number of seconds in the `Retry-After` header.\n- `internal_error`: something went wrong in ContactZapp. Try again.\n- `unavailable`: ContactZapp could not check the request just now. Try again.\n- `invalid_parameter`: a parameter is not usable, for example a limit out of range, a cursor from another request, or more than one filter.\n- `not_found`: there is nothing at this address for this API key.\n- `invalid_body`: the request body is not a contact.\n- `invalid_date`: a date in the request is not a real date.\n- `empty_contact`: the contact has no values.\n- `no_account_to_write_to`: the user has no two-way account that keeps an address book and is not paused.\n- `contact_gone`: the contact was merged into another contact or deleted.\n- `contact_pending_review`: the contact waits in Review duplicates.\n- `contact_changed`: the contact changed while it was being written. Try again.\n- `idempotency_key_invalid`: the `Idempotency-Key` header is empty, too long or has more than one value.\n- `idempotency_key_reused`: this `Idempotency-Key` was already used with a different request.\n- `idempotency_key_in_progress`: a request with this `Idempotency-Key` is still being handled, or did not finish.\n- `body_too_large`: the request body is too large.\n- `unknown_field`: the body carries a field a contact write does not take: a misspelt name, a read-only field such as `id` or `updatedAt`, or a field the contact does not show. Nothing was written.\n",
        "enum": [
          "api_key_missing",
          "api_key_invalid",
          "insufficient_scope",
          "rate_limited",
          "internal_error",
          "unavailable",
          "invalid_parameter",
          "not_found",
          "invalid_body",
          "invalid_date",
          "empty_contact",
          "no_account_to_write_to",
          "contact_gone",
          "contact_pending_review",
          "contact_changed",
          "idempotency_key_invalid",
          "idempotency_key_reused",
          "idempotency_key_in_progress",
          "body_too_large",
          "unknown_field"
        ]
      },
      "V1Error": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "$ref": "#/components/schemas/V1ErrorCode"
          },
          "message": {
            "type": "string",
            "description": "A sentence for a person, in English."
          }
        }
      }
    }
  }
}
