{
  "openapi": "3.1.0",
  "info": {
    "title": "Envoke Contact API",
    "version": "1.0.0",
    "description": "Read, create, update, and delete the contacts in an Envoke account: profile fields, consent, custom field values, and tag, subscription, mandatory consent, and nurture campaign (autoresponder) membership. All endpoints require HTTP Basic Auth and exchange JSON. The Contact API answers on `/v1` only; there is no `/v2/contacts`. Points a client must know, because they differ from Envoke's newer APIs: (1) There are two error envelopes. Body and query errors return `result_text` `Validation failed, see 'result_data' -> 'errors'` with `result_data.errors` as an array of strings; field checks that run afterwards (field lengths, email format, the account's duplicate check) return `result_data.fields` keyed by database column (`fname`, `lname`, `addr1`, `addr2`, `postal`, `eid`) with the messages joined by `, ` in `result_text`. Request-shape and lookup errors (`Invalid request`, `Invalid JSON`, id mismatch, key-field lookup failures) use a plain envelope with `result_data: []`. (2) `limit=1` on the list endpoint returns the matching contact as a bare object, not an array of one, and `result_type=kvp` is ignored with it. (3) A bulk `PATCH /contacts` with a list body returns HTTP 400 and `success: false` if any contact fails, even though the others were updated. (4) Delete responses carry only `success` and `result_text`; there is no `result_data` key. (5) `id` is a string everywhere except `result_data.id` of `PATCH /contacts/{id}` and the per-contact results of a bulk update, where it is an integer. (6) An unrecognised `country` or `language` is a warning, not an error; where the warning appears depends on the endpoint. (7) An empty body, `{}`, or `[]` returns `Invalid request`; a body that is not valid JSON returns `Invalid JSON`. All error responses use HTTP 400, except authentication and account errors: invalid credentials return 401 with `result_text` `Invalid credentials` and `result_data: []`; a valid key on a closed or inactive account returns 403 with the plain-text body `This account has been closed.` or `This account is not active.`; exceeding the account's API limit returns 429 with `success: false` and a `result_text` naming the limit."
  },
  "servers": [
    {
      "url": "https://e1.envoke.com/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "basicAuth": []
    }
  ],
  "tags": [
    {
      "name": "Contacts",
      "description": "Operations on contacts."
    }
  ],
  "components": {
    "securitySchemes": {
      "basicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "HTTP Basic Auth. Use your API ID as the username and API Key as the password. See https://developers.envoke.com/guides/authentication/."
      }
    },
    "parameters": {
      "ContactId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The Envoke contact ID.",
        "schema": {
          "type": "integer",
          "minimum": 1
        },
        "example": 1001
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Maximum number of contacts to return. Default 10, hard cap 100. When `fields` or `result_type=kvp` is also set, the default becomes 10,000 and a value above 100 is honoured as-is. `limit=1` returns a single contact as a bare object instead of an array (or `[]` when nothing matches). A value that is not a whole number gives `Invalid 'limit': '{value}'`.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 10
        }
      },
      "Skip": {
        "name": "skip",
        "in": "query",
        "description": "Number of contacts to skip. Use with `limit` and `sort` to paginate. A value that is not a whole number gives `Invalid 'skip': '{value}'`.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      },
      "Fields": {
        "name": "fields",
        "in": "query",
        "description": "Comma-separated list of fields to include in each result, e.g. `id,email`. On the list endpoint, setting it raises the default limit to 10,000 and lifts the 100 cap.",
        "schema": {
          "type": "string"
        },
        "example": "id,email,consent_status"
      },
      "ResultType": {
        "name": "result_type",
        "in": "query",
        "description": "Set to `kvp` to receive an object of contact ID to email address (`{ \"1001\": \"alto.lead@example.com\" }`) instead of an array. Raises the default limit to 10,000 and lifts the 100 cap. Has no effect when `limit=1`.",
        "schema": {
          "type": "string",
          "enum": [
            "kvp"
          ]
        }
      },
      "KeyFieldCreate": {
        "name": "key_field",
        "in": "query",
        "description": "The field used to check whether the contact already exists: `email` (default) or `remote_id`. With `remote_id`, the body must include `remote_id`. Any other value gives `Invalid 'key_field' value: '{value}'`. The parameter is `key_field`; the misspelling `keyfield` is ignored without an error.",
        "schema": {
          "type": "string",
          "enum": [
            "email",
            "remote_id"
          ],
          "default": "email"
        }
      },
      "KeyFieldUpdate": {
        "name": "key_field",
        "in": "query",
        "description": "How `PATCH /contacts` with an object body finds the contact: `id` (looked up), `email`, or `remote_id`. When not set, a body `id` is used directly without a lookup, otherwise `email`. Ignored for list bodies and by `PATCH /contacts/{id}`. Any other value gives `Invalid 'key_field' value: '{value}'`.",
        "schema": {
          "type": "string",
          "enum": [
            "id",
            "email",
            "remote_id"
          ]
        }
      },
      "Upsert": {
        "name": "upsert",
        "in": "query",
        "description": "When `1`, `true`, `yes`, or `on`, an existing contact matched by `key_field` is updated instead of rejected as a duplicate. An empty value (`?upsert=`) is false. An upsert that updates ignores empty values (`write_empty_values` has no effect on POST, though a custom field value of `\"\"` still deletes that value), does not default `consent_status` to `Not Provided`, ignores `id` in the body, and appends warnings after `. `, giving a double period.",
        "schema": {
          "type": "boolean",
          "default": false
        }
      },
      "WriteEmptyValues": {
        "name": "write_empty_values",
        "in": "query",
        "description": "When `1`, `true`, `yes`, or `on`, an empty string (`\"\"`) clears a text field. Otherwise `\"\"`, `0`, `\"0\"`, `false`, and `[]` are ignored; `null` is always ignored. A custom field value of `\"\"` deletes that value with or without this parameter. `email` cannot be cleared (`Email must not be empty`), and `country` and `language` cannot be cleared.",
        "schema": {
          "type": "boolean",
          "default": false
        }
      }
    },
    "schemas": {
      "Contact": {
        "type": "object",
        "description": "A contact as returned by GET and by the create, upsert, and key-field update responses. Every field is present unless `fields` selects some. An unset value is `\"\"`, never `null` (a stored `\"0\"` is also returned as `\"\"`).",
        "required": [
          "id",
          "external_id",
          "date_created",
          "remote_id",
          "first_name",
          "last_name",
          "title",
          "email",
          "company",
          "phone",
          "address_1",
          "address_2",
          "city",
          "country",
          "province",
          "postal_code",
          "website",
          "language",
          "feedback",
          "consent_status",
          "consent_description",
          "consent_effective_date",
          "consent_expiry",
          "custom_fields",
          "interests",
          "autoresponders"
        ],
        "properties": {
          "id": {
            "type": "string",
            "readOnly": true,
            "description": "Envoke contact ID, assigned on creation. Returned as a string.",
            "examples": [
              "1001"
            ]
          },
          "external_id": {
            "type": "string",
            "readOnly": true,
            "description": "32-character identifier used in public-facing links such as preference pages. Not the same as `id`. Ignored on write.",
            "examples": [
              "9f2c4e7a1b3d5f6a8c0e2b4d6f8a1c3e"
            ]
          },
          "date_created": {
            "type": "string",
            "format": "date-time",
            "readOnly": true,
            "description": "When the contact was created, W3C format in UTC. Ignored on write. Filterable with the range operators.",
            "examples": [
              "2026-10-05T14:03:11+00:00"
            ]
          },
          "remote_id": {
            "type": "string",
            "maxLength": 60,
            "description": "Your own identifier for the contact, e.g. a CRM or membership ID. Usable as the key field on create and update. Too long: `Remote ID must not exceed 60 characters`.",
            "examples": [
              "HCS-0002",
              ""
            ]
          },
          "first_name": {
            "type": "string",
            "maxLength": 50,
            "description": "First name. Too long: `First name must not exceed 50 characters`.",
            "examples": [
              "Maya"
            ]
          },
          "last_name": {
            "type": "string",
            "maxLength": 50,
            "description": "Last name. Too long: `Last name must not exceed 50 characters`.",
            "examples": [
              "Okafor"
            ]
          },
          "title": {
            "type": "string",
            "maxLength": 50,
            "description": "Job title or honorific. Too long: `Title must not exceed 50 characters`.",
            "examples": [
              "Section Lead"
            ]
          },
          "email": {
            "type": "string",
            "maxLength": 60,
            "description": "Email address. Required on create, even with `key_field=remote_id`. Can be changed on update but not cleared. The account's duplicate check (usually on `email`) rejects a value that belongs to another contact: `contact with email = {value} already exists`.",
            "examples": [
              "alto.lead@example.com"
            ]
          },
          "company": {
            "type": "string",
            "maxLength": 80,
            "description": "Company or organisation. Too long: `Company must not exceed 80 characters`.",
            "examples": [
              "Harbourfront Choral Society",
              ""
            ]
          },
          "phone": {
            "type": "string",
            "maxLength": 25,
            "description": "Phone number, free text with no format check. Too long: `Phone must not exceed 25 characters`.",
            "examples": [
              "416-555-0142"
            ]
          },
          "address_1": {
            "type": "string",
            "maxLength": 100,
            "description": "Street address, line 1. Too long: `Address must not exceed 100 characters`.",
            "examples": [
              "48 Harbour Lane"
            ]
          },
          "address_2": {
            "type": "string",
            "maxLength": 100,
            "description": "Street address, line 2. Too long: `Address 2 must not exceed 100 characters`.",
            "examples": [
              "Unit 5"
            ]
          },
          "city": {
            "type": "string",
            "maxLength": 50,
            "description": "City. Too long: `City must not exceed 50 characters`.",
            "examples": [
              "Toronto"
            ]
          },
          "country": {
            "type": "string",
            "description": "Returned as a 2-letter ISO code, or `\"\"`. Accepts an ISO code (`CA`) or a recognised country name (`Canada`, `France`, `U.S.A.`, `UK`). An unrecognised value is not saved and produces the warning `'{value}' is not a valid value for country; field will not be updated`. Decides which consent rules apply; a contact with no country uses the account's default consent country. Cannot be cleared.",
            "examples": [
              "CA",
              ""
            ]
          },
          "province": {
            "type": "string",
            "maxLength": 50,
            "description": "Province or state, free text. Too long: `Province must not exceed 50 characters`.",
            "examples": [
              "ON"
            ]
          },
          "postal_code": {
            "type": "string",
            "maxLength": 15,
            "description": "Postal or ZIP code. Too long: `Postal / zip must not exceed 15 characters`.",
            "examples": [
              "M5V 0Z9"
            ]
          },
          "website": {
            "type": "string",
            "maxLength": 255,
            "description": "Website URL, free text with no URL check. Too long: `Website must not exceed 255 characters`.",
            "examples": [
              "https://example.org",
              ""
            ]
          },
          "language": {
            "type": "string",
            "enum": [
              "en",
              "fr",
              ""
            ],
            "description": "Preferred language, returned as `en`, `fr`, or `\"\"`. Accepts `en`, `fr`, `English`, or `French`, case-insensitive. Any other value is not saved and produces the warning `'{value}' is not a valid value for language; field will not be updated`. Cannot be cleared.",
            "examples": [
              "en"
            ]
          },
          "feedback": {
            "type": "string",
            "description": "Free-text notes. HTML tags are removed before the value is saved, without an error.",
            "examples": [
              "Happy to help with ushering at the winter concert.",
              ""
            ]
          },
          "consent_status": {
            "type": "string",
            "description": "Name of the contact's current consent type. Writable values: `Express`, `Implied - Inquiry`, `Implied - Transaction`, `Implied - No Expiry`, `Not Provided`, `Revoked`, `Envoke Spam Reported`. Read-only values set by Envoke: `Expired` (implied consent that has expired) and `External Spam Reported` (from a spam complaint). Older contacts can also carry the retired types `Implied - Existing Relationship`, `Legacy Subscribed`, `Legacy Confirmed`, or `Legacy Unknown`, which cannot be written. Defaults to `Not Provided` on create. A downgrade is recorded but not made current unless `consent_force_downgrade` is sent; changes away from a spam-reported type, and `Revoked` to `Not Provided`, are blocked. Neither is reported in the response.",
            "examples": [
              "Express"
            ]
          },
          "consent_description": {
            "type": "string",
            "maxLength": 500,
            "description": "How and when consent was obtained. Text beyond 500 characters is cut off without an error. Never required.",
            "examples": [
              "Signed up at the spring concert"
            ]
          },
          "consent_effective_date": {
            "type": "string",
            "format": "date-time",
            "description": "When the current consent took effect, W3C format in UTC. On write, a value without a time zone is read as UTC; a date with no time is read as midnight in Envoke's server time zone. Defaults to the time of the request. Must not be in the future (`consent_effective_date cannot be in the future.`); an unreadable value gives `Invalid date format for consent_effective_date`. Filterable with the range operators; not sortable.",
            "examples": [
              "2026-09-12T18:30:00+00:00"
            ]
          },
          "consent_expiry": {
            "type": "string",
            "readOnly": true,
            "description": "Expiry date of the current consent as `YYYY-MM-DD`, or `\"\"`. `Implied - Inquiry` expires 6 months, and `Implied - Transaction` 2 years, after the effective date. On GET it is `\"\"` when the consent does not expire, when the contact's country is not `CA` (no country uses the account's default consent country), or when the account ignores consent expiry; create, upsert, and `PATCH /contacts` responses are not blanked. Ignored on write. Filterable with the range operators as a plain date; not sortable.",
            "examples": [
              "",
              "2027-03-12"
            ]
          },
          "custom_fields": {
            "type": "object",
            "description": "Every custom field that has a value for the contact, keyed by the custom field's name exactly as defined in the account. `{}` when there are none. Lead-scope (conversion page) fields are not returned.",
            "additionalProperties": {
              "type": "string"
            },
            "examples": [
              {
                "Voice_Part": "Alto",
                "Season_Ticket_Number": "HCS-ST-0417"
              }
            ]
          },
          "interests": {
            "type": "array",
            "description": "Names of every tag, subscription, and mandatory consent item the contact has. `[]` when none.",
            "items": {
              "type": "string"
            },
            "examples": [
              [
                "Volunteer",
                "Alumni",
                "Concert News"
              ]
            ]
          },
          "autoresponders": {
            "type": "array",
            "description": "Names of the nurture campaigns (autoresponders) the contact is on. `[]` when none.",
            "items": {
              "type": "string"
            },
            "examples": [
              [
                "Welcome Sequence"
              ],
              []
            ]
          }
        }
      },
      "InterestsWrite": {
        "description": "Tags, subscriptions, and mandatory consent items to write. A list of names adds each one and removes nothing. An object maps a name (or the item's numeric ID) to a state: `\"Set\"` or `1` adds, `\"Unset\"` or `0` removes. Names match ignoring case and treating a space and an underscore as the same. Numeric states must be JSON numbers; a string such as `\"1\"` is read as a name. Unknown name: `Invalid 'interest': '{name}' (for a list of valid interests try the endpoint '/interests')`. Wrong type: `'interests' must be an object or array`. Adding a subscription does not check the contact's sending consent; a `Not Provided` contact is subscribed like any other.",
        "oneOf": [
          {
            "type": "array",
            "items": {
              "type": "string"
            },
            "examples": [
              [
                "Volunteer",
                "Concert News"
              ]
            ]
          },
          {
            "type": "object",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "string",
                  "enum": [
                    "Set",
                    "Unset"
                  ]
                },
                {
                  "type": "integer",
                  "enum": [
                    1,
                    0
                  ]
                }
              ]
            },
            "examples": [
              {
                "Volunteer": "Set",
                "Alumni": "Unset"
              }
            ]
          }
        ]
      },
      "AutorespondersWrite": {
        "description": "Nurture campaigns to write. A list of names adds and queues each one. An object maps a name (or the campaign's numeric ID) to a state: `\"Set\"`, `1`, or `2` adds and queues; `\"Unset\"` or `0` removes; `\"Dequeue\"` or `-1` stops further messages but leaves the contact on the campaign. Unknown name: `Invalid 'autoresponder': '{name}' (for a list of valid autoresponders try the endpoint '/autoresponders')`. Wrong type: `'autoresponders' must be an object or array`. A campaign is not added (the contact is not put on it) when the contact's consent does not allow sending, unless the campaign is transactional (never for a spam-reported contact); this is not reported. Campaigns cannot be created through the API.",
        "oneOf": [
          {
            "type": "array",
            "items": {
              "type": "string"
            },
            "examples": [
              [
                "Welcome Sequence"
              ]
            ]
          },
          {
            "type": "object",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "string",
                  "enum": [
                    "Set",
                    "Unset",
                    "Dequeue"
                  ]
                },
                {
                  "type": "integer",
                  "enum": [
                    2,
                    1,
                    0,
                    -1
                  ]
                }
              ]
            },
            "examples": [
              {
                "Welcome Sequence": "Dequeue"
              }
            ]
          }
        ]
      },
      "ContactWritable": {
        "type": "object",
        "description": "Writable contact fields shared by create and update. A key that is not a contact field gives `'{key}' is not a valid contact field`. `external_id`, `date_created`, and `consent_expiry` are read-only and silently ignored.",
        "properties": {
          "remote_id": {
            "type": "string",
            "maxLength": 60,
            "examples": [
              "HCS-0002"
            ]
          },
          "first_name": {
            "type": "string",
            "maxLength": 50,
            "examples": [
              "Maya"
            ]
          },
          "last_name": {
            "type": "string",
            "maxLength": 50,
            "examples": [
              "Okafor"
            ]
          },
          "title": {
            "type": "string",
            "maxLength": 50,
            "examples": [
              "Section Lead"
            ]
          },
          "email": {
            "type": "string",
            "maxLength": 60,
            "description": "Must be a JSON string. Errors: `Email is required`, `Email must be a string`, `Email invalid`, `Email must not exceed 60 characters`; on update, `Email must not be empty`.",
            "examples": [
              "alto.lead@example.com"
            ]
          },
          "company": {
            "type": "string",
            "maxLength": 80,
            "examples": [
              "Harbourfront Choral Society"
            ]
          },
          "phone": {
            "type": "string",
            "maxLength": 25,
            "examples": [
              "416-555-0142"
            ]
          },
          "address_1": {
            "type": "string",
            "maxLength": 100,
            "examples": [
              "48 Harbour Lane"
            ]
          },
          "address_2": {
            "type": "string",
            "maxLength": 100,
            "examples": [
              "Unit 5"
            ]
          },
          "city": {
            "type": "string",
            "maxLength": 50,
            "examples": [
              "Toronto"
            ]
          },
          "country": {
            "type": "string",
            "description": "ISO code or recognised country name. Unrecognised values produce a warning and are not saved.",
            "examples": [
              "Canada",
              "CA"
            ]
          },
          "province": {
            "type": "string",
            "maxLength": 50,
            "examples": [
              "ON"
            ]
          },
          "postal_code": {
            "type": "string",
            "maxLength": 15,
            "examples": [
              "M5V 0Z9"
            ]
          },
          "website": {
            "type": "string",
            "maxLength": 255,
            "examples": [
              "https://example.org"
            ]
          },
          "language": {
            "type": "string",
            "description": "`en`, `fr`, `English`, or `French`, case-insensitive. Other values produce a warning and are not saved.",
            "examples": [
              "English",
              "en"
            ]
          },
          "feedback": {
            "type": "string",
            "description": "HTML is stripped.",
            "examples": [
              "Happy to help with ushering at the winter concert."
            ]
          },
          "consent_status": {
            "type": "string",
            "enum": [
              "Express",
              "Implied - Inquiry",
              "Implied - Transaction",
              "Implied - No Expiry",
              "Not Provided",
              "Revoked",
              "Envoke Spam Reported"
            ],
            "description": "Case-insensitive; a space and an underscore are treated as the same character. An unknown value gives `Invalid 'consent_status': '{value}' (possible values: 'Express', 'Implied - Inquiry', 'Implied - Transaction', 'Implied - No Expiry', 'Not Provided', 'Revoked', 'Envoke Spam Reported')`. Implied types do not apply to contacts in the EU consent region; a sentence is appended to `result_text` instead.",
            "examples": [
              "Express"
            ]
          },
          "consent_description": {
            "type": "string",
            "maxLength": 500,
            "examples": [
              "Signed up at the spring concert"
            ]
          },
          "consent_effective_date": {
            "type": "string",
            "description": "Date and time, e.g. `2026-09-12T18:30:00Z` or `2026-09-12 18:30:00` (UTC). Not in the future.",
            "examples": [
              "2026-09-12T18:30:00Z"
            ]
          },
          "consent_force_downgrade": {
            "type": [
              "boolean",
              "string",
              "integer"
            ],
            "writeOnly": true,
            "description": "Sent with `consent_status` to make a preempted downgrade current. True for `true`, `\"true\"`, `1`, `\"1\"`, `\"yes\"`, `\"on\"`, or `\"Set\"`; anything else is false. Does nothing on its own, on a new contact, or for blocked changes. Never returned.",
            "examples": [
              true
            ]
          },
          "custom_fields": {
            "type": "object",
            "description": "Values keyed by custom field name; matching ignores case and treats a space and an underscore as the same. IDs are not accepted as keys. Values are stored as text and not checked against the field's type. `\"\"` deletes the value; a value that cannot be stored (e.g. `null`) is skipped without an error. Fields not included are left unchanged. Unknown key: `Invalid 'custom_field': '{name}' (for a list of valid custom fields try the endpoint '/customfields')`. Not an object: `'custom_fields' must be an object`.",
            "additionalProperties": {
              "type": "string"
            },
            "examples": [
              {
                "Voice_Part": "Alto",
                "Season_Ticket_Number": "HCS-ST-0417"
              }
            ]
          },
          "interests": {
            "$ref": "#/components/schemas/InterestsWrite"
          },
          "autoresponders": {
            "$ref": "#/components/schemas/AutorespondersWrite"
          }
        }
      },
      "ContactCreate": {
        "description": "Request body for `POST /contacts`: a single JSON object. `email` is required; `remote_id` is also required with `key_field=remote_id`. `id` is ignored. A missing `consent_status` becomes `Not Provided` on a new contact.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ContactWritable"
          },
          {
            "type": "object",
            "required": [
              "email"
            ]
          }
        ]
      },
      "ContactUpdate": {
        "description": "Request body for one contact in a PATCH. Send only the fields to change; only fields that differ from the stored value are written, and a request that changes nothing still succeeds. `id` identifies the contact and is never written.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ContactWritable"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": [
                  "integer",
                  "string"
                ],
                "description": "On `PATCH /contacts/{id}`, must equal the URL id if sent (`\"1001\"` and `1001` count as the same). On `PATCH /contacts` without `key_field`, and in a list body, used directly without a lookup.",
                "examples": [
                  1001
                ]
              }
            }
          }
        ]
      },
      "CreateSuccessResponse": {
        "type": "object",
        "description": "Returned by a create, and by an upsert whether it creates or updates. `result_data` is the full contact. Warnings are appended to `result_text`: after a space, joined with `. `, on create; after `. ` (a double period) when an upsert updates. For an EU contact given an implied type, a consent sentence is appended in the same way.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "result_text": {
            "type": "string",
            "examples": [
              "The contact was successfully created.",
              "The contact was successfully updated."
            ]
          },
          "result_data": {
            "$ref": "#/components/schemas/Contact"
          }
        }
      },
      "UpdateByIdSuccessResponse": {
        "type": "object",
        "description": "Returned by `PATCH /contacts/{id}`. A short result, not the full contact.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "result_text": {
            "type": "string",
            "description": "`The contact was successfully updated.`, followed by a consent sentence when an EU rule applied.",
            "examples": [
              "The contact was successfully updated."
            ]
          },
          "result_data": {
            "type": "object",
            "required": [
              "id",
              "submit_type",
              "warnings"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "description": "The contact ID, as an integer on this endpoint.",
                "examples": [
                  1001
                ]
              },
              "submit_type": {
                "type": "string",
                "const": "update"
              },
              "warnings": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "`country` and `language` warnings; `[]` when none.",
                "examples": [
                  [],
                  [
                    "'Narnia' is not a valid value for country; field will not be updated"
                  ]
                ]
              },
              "no_affected_rows": {
                "type": "boolean",
                "const": true,
                "description": "Present when no profile column changed (only custom fields, tags, nurture campaigns, or consent, or nothing at all)."
              },
              "inapplicable_consent_updated": {
                "type": "boolean",
                "const": true,
                "description": "Present when a change of `country` to an EU country reset an implied consent type to `Not Provided`."
              },
              "consent_fix_message": {
                "type": "string",
                "description": "Present with `inapplicable_consent_updated`; the sentence also appended to `result_text`.",
                "examples": [
                  " Their consent status was updated because Implied - Inquiry consent does not apply to contacts in the EU."
                ]
              }
            }
          }
        }
      },
      "UpdateByKeySuccessResponse": {
        "type": "object",
        "description": "Returned by `PATCH /contacts` with an object body. `result_data` is the full, updated contact. Warnings are appended to `result_text` after `. `, which follows the success message's own period.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "result_text": {
            "type": "string",
            "examples": [
              "The contact was successfully updated.",
              "The contact was successfully updated.. 'Narnia' is not a valid value for country; field will not be updated"
            ]
          },
          "result_data": {
            "$ref": "#/components/schemas/Contact"
          }
        }
      },
      "BulkUpdateResponse": {
        "type": "object",
        "description": "Returned by `PATCH /contacts` (or `PATCH /contacts/{id}`, whose id is then ignored) with a list body. HTTP 200 when every contact was updated; HTTP 400 with `success: false` when any failed, even though the others were updated.",
        "required": [
          "success",
          "successes",
          "failures",
          "result_data",
          "result_text"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "successes": {
            "type": "array",
            "items": {
              "type": [
                "string",
                "integer"
              ]
            },
            "description": "IDs of the contacts that were updated.",
            "examples": [
              [
                "1006",
                "1007"
              ]
            ]
          },
          "failures": {
            "type": "array",
            "items": {
              "type": [
                "string",
                "integer"
              ]
            },
            "description": "IDs of contacts that were found but failed, and `CONTACT_OBJECT_{n}` (zero-based list position) for contacts that could not be found.",
            "examples": [
              [
                "CONTACT_OBJECT_2"
              ]
            ]
          },
          "result_data": {
            "type": "object",
            "description": "Each contact's result keyed by contact ID or `CONTACT_OBJECT_{n}`. A found contact has the `result_data` of `PATCH /contacts/{id}` (or its `errors`, or `fields` and `warnings`); a contact that could not be found has `errors` with the lookup error.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "submit_type": {
                  "type": "string"
                },
                "warnings": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "errors": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "fields": {
                  "type": [
                    "object",
                    "array"
                  ]
                }
              }
            }
          },
          "result_text": {
            "type": "string",
            "examples": [
              "All contacts were successfully updated",
              "2 contacts were successfully updated; 1 contacts were not updated"
            ]
          }
        }
      },
      "DeleteResponse": {
        "type": "object",
        "description": "Returned by DELETE, on success and on failure. There is no `result_data` key.",
        "required": [
          "success",
          "result_text"
        ],
        "additionalProperties": false,
        "properties": {
          "success": {
            "type": "boolean"
          },
          "result_text": {
            "type": "string",
            "examples": [
              "1 contact deleted",
              "1 contact not deleted, 1 - no matching contact found",
              "1 contact not deleted, 1 - can't delete due to consent status"
            ]
          }
        }
      },
      "ValidationErrorResponse": {
        "type": "object",
        "description": "Body, query, and not-found-by-id errors. Every problem found by the check is listed.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "result_text": {
            "type": "string",
            "const": "Validation failed, see 'result_data' -> 'errors'"
          },
          "result_data": {
            "type": "object",
            "required": [
              "errors"
            ],
            "properties": {
              "errors": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "FieldValidationErrorResponse": {
        "type": "object",
        "description": "Field lengths, email format, and the account's duplicate check, which run after the body checks. `result_data.fields` is keyed by database column (`first_name` is `fname`, `last_name` `lname`, `address_1` `addr1`, `address_2` `addr2`, `postal_code` `postal`, `external_id` `eid`; others match the API name), and `result_text` holds the messages joined with `, `. A duplicate-check failure has `fields: []`. On update, the envelope also carries `warnings`.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "result_text": {
            "type": "string",
            "examples": [
              "Email invalid, First name must not exceed 50 characters",
              "contact with email = alto.lead@example.com already exists"
            ]
          },
          "result_data": {
            "type": "object",
            "required": [
              "fields"
            ],
            "properties": {
              "fields": {
                "type": [
                  "object",
                  "array"
                ],
                "additionalProperties": {
                  "type": "string"
                },
                "examples": [
                  {
                    "email": "Email invalid",
                    "fname": "First name must not exceed 50 characters"
                  },
                  []
                ]
              },
              "warnings": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "PlainErrorResponse": {
        "type": "object",
        "description": "Request-shape and lookup errors: the message is in `result_text` and `result_data` is an empty array.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "result_text": {
            "type": "string",
            "examples": [
              "Invalid request",
              "Invalid JSON",
              "Contact 'id': 1001 does not match 'id' in request body (1002)",
              "No contact was found with the 'email': 'nobody@example.com'"
            ]
          },
          "result_data": {
            "type": "array",
            "maxItems": 0
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Invalid API ID or key. A `WWW-Authenticate: Basic` header is sent, and the body is the plain error envelope. The response's `Content-Type` is `text/html`, although the body is JSON.",
        "content": {
          "text/html": {
            "schema": {
              "$ref": "#/components/schemas/PlainErrorResponse"
            },
            "example": {
              "success": false,
              "result_data": [],
              "result_text": "Invalid credentials"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Valid key on a closed or inactive account.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": false
                },
                "result_data": {
                  "type": "array",
                  "maxItems": 0
                },
                "result_text": {
                  "type": "string",
                  "enum": [
                    "This account has been closed.",
                    "This account is not active."
                  ]
                }
              },
              "required": [
                "success",
                "result_data",
                "result_text"
              ]
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "The account's API limit was exceeded.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": {
                  "type": "boolean",
                  "const": false
                },
                "result_text": {
                  "type": "string",
                  "description": "Names the limit that was exceeded."
                },
                "result_data": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/contacts": {
      "get": {
        "summary": "List contacts",
        "description": "Returns a JSON array of contacts, or `[]` when none match. Supports filtering, sorting, pagination, and field selection. Without `sort`, the order is not defined. `limit=1` returns a bare object (or `[]`). Filter operators: equals and `(ne)` on every filterable field; `(regex)` on `custom_fields.{name}` only; `(gt)`, `(lt)`, `(gte)`, `(lte)` on `date_created`, `consent_effective_date`, and `consent_expiry` only; `(or)` and `(not)` groups. Each operator also has a `$` spelling (`$ne`, `$regex`, ...). `(in)` is not supported on any contacts field; use `(or)`. Separate filters are ANDed, and two operators on one field are ANDed. When the request has no `filter` parameter, every query parameter other than `limit`, `skip`, `sort`, `fields`, and `result_type` is treated as a filter, so `?email=alto.lead%40example.com` equals `?filter[email]=alto.lead%40example.com`, and a stray parameter is rejected (`Field 'key_field' is not valid for filtering`). Invalid parameters return 400 with every problem listed.",
        "operationId": "listContacts",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Skip"
          },
          {
            "$ref": "#/components/parameters/Fields"
          },
          {
            "$ref": "#/components/parameters/ResultType"
          },
          {
            "name": "filter[{field}]",
            "in": "query",
            "description": "Profile fields: `id`, `external_id`, `remote_id`, `first_name`, `last_name`, `title`, `email`, `company`, `phone`, `address_1`, `address_2`, `city`, `country`, `province`, `postal_code`, `website`, `language`, `feedback`. Operators: equals and `(ne)`, e.g. `filter[city][(ne)]=Toronto`. `country` is matched against the stored 2-letter code (`CA`, not `Canada`). Another operator gives `'{SQL operator}' is not a valid operator for the field '{field}'` (e.g. `'REGEXP' ...` for `(regex)`, `'IN' ...` for `(in)`); an unknown field gives `Field '{field}' is not valid for filtering`; a list given to an operator (`filter[email][(ne)][]=...`) gives `Field '{field}' requires a scalar value`; a list with no operator (`filter[email][]=...`) gives `Field '0' is not valid for filtering`.",
            "schema": {
              "type": "string"
            },
            "example": "Toronto"
          },
          {
            "name": "filter[date_created]",
            "in": "query",
            "description": "Operators: equals, `(ne)`, `(gt)`, `(lt)`, `(gte)`, `(lte)`. Read as UTC unless the value has a time zone; a date with no time is read as midnight in Envoke's server time zone. An unreadable value gives `Invalid date format in filter condition for date_created`.",
            "schema": {
              "type": "string"
            },
            "example": "2026-01-01T00:00:00Z"
          },
          {
            "name": "filter[consent_status]",
            "in": "query",
            "description": "Consent type name. Operators: equals and `(ne)`.",
            "schema": {
              "type": "string"
            },
            "example": "Express"
          },
          {
            "name": "filter[consent_description]",
            "in": "query",
            "description": "Operators: equals and `(ne)`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter[consent_effective_date]",
            "in": "query",
            "description": "Operators: equals, `(ne)`, `(gt)`, `(lt)`, `(gte)`, `(lte)`. Read as UTC unless the value has a time zone.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01T00:00:00Z"
          },
          {
            "name": "filter[consent_expiry]",
            "in": "query",
            "description": "Operators: equals, `(ne)`, `(gt)`, `(lt)`, `(gte)`, `(lte)`. Compared as a plain `YYYY-MM-DD` date with no time zone conversion, against the stored expiry (GET blanking does not apply to filtering).",
            "schema": {
              "type": "string"
            },
            "example": "2027-03-31"
          },
          {
            "name": "filter[custom_fields.{name}]",
            "in": "query",
            "description": "A custom field, by its name exactly as defined (case-sensitive; names contain only letters, digits, and underscores), e.g. `filter[custom_fields.Voice_Part]=Alto`. Operators: equals, `(ne)`, and `(regex)` (a MySQL regular expression). `(ne)` matches only contacts that have a value for the field. Use the `filter[...]` form for custom fields: dots and spaces in a bare parameter name are not preserved. The bare name `filter[custom_fields]` is not a valid filter.",
            "schema": {
              "type": "string"
            },
            "example": "Alto"
          },
          {
            "name": "filter[interests]",
            "in": "query",
            "description": "Exact, case-sensitive name of a tag, subscription, or mandatory consent item; matches contacts that have it, and `(ne)` matches contacts that don't. IDs are not accepted. Operators: equals and `(ne)`. An unknown name gives `Invalid 'interest' provided: '{value}'`.",
            "schema": {
              "type": "string"
            },
            "example": "Volunteer"
          },
          {
            "name": "filter[autoresponders]",
            "in": "query",
            "description": "Exact name of a nurture campaign; works like `filter[interests]`. Operators: equals and `(ne)`. An unknown name gives `Invalid 'autoresponder' provided: '{value}'`.",
            "schema": {
              "type": "string"
            },
            "example": "Welcome Sequence"
          },
          {
            "name": "filter[(or)]",
            "in": "query",
            "description": "Matches if any of the numbered groups inside match: `filter[(or)][0][email]=a%40example.com&filter[(or)][1][email]=b%40example.com`. `$or` is the same.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter[(not)]",
            "in": "query",
            "description": "Matches if the conditions inside do not all match: `filter[(not)][city]=Toronto&filter[(not)][language]=fr` excludes contacts in Toronto who are also French-speaking. `$not` is the same.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter[{n}]",
            "in": "query",
            "description": "Numbered AND group, used to repeat a field: `filter[0][interests]=Volunteer&filter[1][interests]=Alumni` matches contacts with both tags.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort[{field}]",
            "in": "query",
            "description": "Sort direction `asc` or `desc` (case-insensitive), or `1` or `-1`. Repeat for a secondary sort. Sortable fields: `id`, `external_id`, `remote_id`, `first_name`, `last_name`, `title`, `email`, `company`, `phone`, `address_1`, `address_2`, `city`, `country`, `province`, `postal_code`, `website`, `language`, `feedback`, `date_created`, `consent_status`, `custom_fields.{name}`. Any other field (including `consent_description`, `consent_effective_date`, `consent_expiry`, `interests`, `autoresponders`) gives `Field '{field}' is not valid for sorting`; a bad direction gives `'{value}' is not a valid sort direction`; `sort` without a field (`?sort=email`) gives `Invalid 'sort': 'email'`.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc",
                "ASC",
                "DESC",
                "1",
                "-1"
              ]
            },
            "example": "asc"
          }
        ],
        "responses": {
          "200": {
            "description": "Matching contacts: an array, a key-value object with `result_type=kvp`, or a single object with `limit=1`.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Contact"
                      }
                    },
                    {
                      "$ref": "#/components/schemas/Contact"
                    },
                    {
                      "type": "object",
                      "description": "Contact ID to email address (`result_type=kvp`).",
                      "additionalProperties": {
                        "type": "string"
                      }
                    }
                  ]
                },
                "examples": {
                  "array": {
                    "summary": "Default array response",
                    "value": [
                      {
                        "id": "1001",
                        "external_id": "9f2c4e7a1b3d5f6a8c0e2b4d6f8a1c3e",
                        "date_created": "2026-10-05T14:03:11+00:00",
                        "remote_id": "",
                        "first_name": "Maya",
                        "last_name": "Okafor",
                        "title": "Section Lead",
                        "email": "alto.lead@example.com",
                        "company": "",
                        "phone": "416-555-0142",
                        "address_1": "48 Harbour Lane",
                        "address_2": "Unit 5",
                        "city": "Toronto",
                        "country": "CA",
                        "province": "ON",
                        "postal_code": "M5V 0Z9",
                        "website": "",
                        "language": "en",
                        "feedback": "",
                        "consent_status": "Express",
                        "consent_description": "Signed up at the spring concert",
                        "consent_effective_date": "2026-09-12T18:30:00+00:00",
                        "consent_expiry": "",
                        "custom_fields": {
                          "Voice_Part": "Alto",
                          "Season_Ticket_Number": "HCS-ST-0417"
                        },
                        "interests": [
                          "Volunteer",
                          "Alumni",
                          "Concert News"
                        ],
                        "autoresponders": []
                      }
                    ]
                  },
                  "fields": {
                    "summary": "Selected fields (fields=id,email,consent_status)",
                    "value": [
                      {
                        "id": "1001",
                        "email": "alto.lead@example.com",
                        "consent_status": "Express"
                      },
                      {
                        "id": "1002",
                        "email": "tenor.remote@example.com",
                        "consent_status": "Express"
                      }
                    ]
                  },
                  "kvp": {
                    "summary": "Key-value response (result_type=kvp)",
                    "value": {
                      "1001": "alto.lead@example.com",
                      "1002": "tenor.remote@example.com"
                    }
                  },
                  "single": {
                    "summary": "limit=1 returns an object (?email=alto.lead%40example.com&fields=id&limit=1)",
                    "value": {
                      "id": "1001"
                    }
                  },
                  "empty": {
                    "summary": "No contacts match",
                    "value": []
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid list parameters. Every problem is listed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorResponse"
                },
                "examples": {
                  "filter_errors": {
                    "summary": "Unknown field and unsupported operator",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "Field 'nickname' is not valid for filtering",
                          "'REGEXP' is not a valid operator for the field 'email'"
                        ]
                      }
                    }
                  },
                  "paging_errors": {
                    "summary": "Bad limit and skip",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "Invalid 'limit': 'ten'",
                          "Invalid 'skip': 'abc'"
                        ]
                      }
                    }
                  },
                  "sort_error": {
                    "summary": "Field cannot be sorted",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "Field 'interests' is not valid for sorting"
                        ]
                      }
                    }
                  },
                  "unknown_interest": {
                    "summary": "Unknown tag name in an interests filter",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "Invalid 'interest' provided: 'Volunteers'"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "summary": "Create a contact",
        "description": "Creates a contact from a single JSON object. `email` is required. The existence check uses `key_field` (`email` by default, or `remote_id`); with `upsert`, an existing match is updated instead of rejected. The email, `key_field`, and duplicate checks run first and return on their own; the remaining body errors are then collected and returned together; field lengths, email format, and the account's duplicate check run last and use the field-validation envelope. Unrecognised `country` and `language` values are warnings appended to `result_text`. Custom field values that cannot be stored and nurture campaigns skipped for lack of consent are not reported.",
        "operationId": "createContact",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/KeyFieldCreate"
          },
          {
            "$ref": "#/components/parameters/Upsert"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactCreate"
              },
              "examples": {
                "minimal": {
                  "summary": "Only an email address (consent becomes Not Provided)",
                  "value": {
                    "email": "to.delete@example.com"
                  }
                },
                "full": {
                  "summary": "Full contact with express consent",
                  "value": {
                    "email": "alto.lead@example.com",
                    "first_name": "Maya",
                    "last_name": "Okafor",
                    "title": "Section Lead",
                    "phone": "416-555-0142",
                    "address_1": "48 Harbour Lane",
                    "address_2": "Unit 5",
                    "city": "Toronto",
                    "province": "ON",
                    "postal_code": "M5V 0Z9",
                    "country": "Canada",
                    "language": "English",
                    "consent_status": "Express",
                    "consent_description": "Signed up at the spring concert",
                    "consent_effective_date": "2026-09-12T18:30:00Z"
                  }
                },
                "custom_fields_and_interests": {
                  "summary": "Custom fields, tags, a subscription, and a nurture campaign",
                  "value": {
                    "email": "alto.lead@example.com",
                    "consent_status": "Express",
                    "custom_fields": {
                      "Voice_Part": "Alto",
                      "Season_Ticket_Number": "HCS-ST-0417"
                    },
                    "interests": [
                      "Volunteer",
                      "Alumni",
                      "Concert News"
                    ],
                    "autoresponders": [
                      "Welcome Sequence"
                    ]
                  }
                },
                "remote_id": {
                  "summary": "Keyed on your own ID (?key_field=remote_id)",
                  "value": {
                    "email": "tenor.remote@example.com",
                    "remote_id": "HCS-0002",
                    "first_name": "Daniel",
                    "last_name": "Reyes"
                  }
                },
                "eu_implied": {
                  "summary": "EU contact with implied consent",
                  "value": {
                    "email": "paris.member@example.org",
                    "first_name": "Camille",
                    "last_name": "Laurent",
                    "country": "France",
                    "consent_status": "Implied - Inquiry"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contact created, or updated by an upsert. `result_data` is the full contact.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateSuccessResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Contact created (result_data shortened)",
                    "value": {
                      "success": true,
                      "result_text": "The contact was successfully created.",
                      "result_data": {
                        "id": "1005",
                        "external_id": "4b8d2f6a0c1e3a5b7d9f1e3c5a7b9d0f",
                        "date_created": "2026-10-05T15:20:44+00:00",
                        "first_name": "Priya",
                        "last_name": "Nair",
                        "email": "upsert.target@example.com",
                        "consent_status": "Not Provided",
                        "consent_effective_date": "2026-10-05T15:20:44+00:00",
                        "consent_expiry": "",
                        "custom_fields": {},
                        "interests": [],
                        "autoresponders": []
                      }
                    }
                  },
                  "created_with_warnings": {
                    "summary": "Unrecognised country and language (result_data omitted)",
                    "value": {
                      "success": true,
                      "result_text": "The contact was successfully created. 'Narnia' is not a valid value for country; field will not be updated. 'de' is not a valid value for language; field will not be updated",
                      "result_data": {
                        "id": "1006",
                        "email": "bulk.a@example.com",
                        "country": "",
                        "language": ""
                      }
                    }
                  },
                  "upsert_updated": {
                    "summary": "Upsert matched an existing contact (result_data shortened)",
                    "value": {
                      "success": true,
                      "result_text": "The contact was successfully updated.",
                      "result_data": {
                        "id": "1005",
                        "email": "upsert.target@example.com",
                        "company": "Harbourfront Choral Society",
                        "consent_status": "Express"
                      }
                    }
                  },
                  "eu_implied": {
                    "summary": "Implied consent does not apply in the EU (result_data shortened)",
                    "value": {
                      "success": true,
                      "result_text": "The contact was successfully created. Their consent status was set to Not Provided because Implied - Inquiry does not apply to contacts in the EU.",
                      "result_data": {
                        "id": "1003",
                        "email": "paris.member@example.org",
                        "country": "FR",
                        "consent_status": "Not Provided",
                        "consent_description": "Implied - Inquiry consent does not apply to contacts in the EU.",
                        "consent_expiry": ""
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error, field-validation error, or unreadable body.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/FieldValidationErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/PlainErrorResponse"
                    }
                  ]
                },
                "examples": {
                  "email_required": {
                    "summary": "Missing email",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "Email is required"
                        ]
                      }
                    }
                  },
                  "duplicate": {
                    "summary": "Email already exists and upsert is not set",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "email value not unique ('alto.lead@example.com' duplicates existing contact with id: '1001')"
                        ]
                      }
                    }
                  },
                  "multiple_matches": {
                    "summary": "More than one contact has the key value (returned even with upsert)",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "Multiple contacts already exist with 'remote_id' value 'HCS-0002'.  (IDs: 1002, 1010)"
                        ]
                      }
                    }
                  },
                  "body_errors": {
                    "summary": "Body errors collected together",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "'nickname' is not a valid contact field",
                          "Invalid 'consent_status': 'Opted In' (possible values: 'Express', 'Implied - Inquiry', 'Implied - Transaction', 'Implied - No Expiry', 'Not Provided', 'Revoked', 'Envoke Spam Reported')"
                        ]
                      }
                    }
                  },
                  "field_validation": {
                    "summary": "Field lengths and email format",
                    "value": {
                      "success": false,
                      "result_text": "Email invalid, First name must not exceed 50 characters",
                      "result_data": {
                        "fields": {
                          "email": "Email invalid",
                          "fname": "First name must not exceed 50 characters"
                        }
                      }
                    }
                  },
                  "duplicate_check": {
                    "summary": "Account duplicate check with key_field=remote_id",
                    "value": {
                      "success": false,
                      "result_text": "contact with email = alto.lead@example.com already exists",
                      "result_data": {
                        "fields": []
                      }
                    }
                  },
                  "invalid_request": {
                    "summary": "Missing, empty, or {} body",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "Invalid request"
                    }
                  },
                  "invalid_json": {
                    "summary": "Body is not valid JSON",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "Invalid JSON"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "summary": "Update one contact by key field, or several contacts",
        "description": "With an object body, finds one contact by `key_field` and returns the full, updated contact; warnings are appended to `result_text` after `. `, giving a double period. Without `key_field`, a body `id` is used directly (not looked up) and otherwise `email` is used, so a body with `id` and a new `email` changes that contact's email. Lookup failures use the plain envelope. With a list body, each object is matched by its `id` (used directly) or else by `email`; `key_field` is ignored, so list items cannot be matched by `remote_id`. Each found contact is updated exactly as by `PATCH /contacts/{id}`. If any contact fails, the response is HTTP 400 with `success: false`, even though the others were updated.",
        "operationId": "updateContacts",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/KeyFieldUpdate"
          },
          {
            "$ref": "#/components/parameters/WriteEmptyValues"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/ContactUpdate"
                  },
                  {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/ContactUpdate"
                    }
                  }
                ]
              },
              "examples": {
                "by_email": {
                  "summary": "One contact found by email",
                  "value": {
                    "email": "alto.lead@example.com",
                    "company": "Harbourfront Choral Society"
                  }
                },
                "by_remote_id": {
                  "summary": "One contact found by remote_id (?key_field=remote_id)",
                  "value": {
                    "remote_id": "HCS-0002",
                    "first_name": "Dan"
                  }
                },
                "bulk": {
                  "summary": "Several contacts",
                  "value": [
                    {
                      "email": "bulk.a@example.com",
                      "city": "Hamilton"
                    },
                    {
                      "email": "bulk.b@example.com",
                      "city": "Hamilton"
                    },
                    {
                      "email": "missing.member@example.com",
                      "city": "Hamilton"
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Object body: the full contact. List body: every contact was updated.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/UpdateByKeySuccessResponse"
                    },
                    {
                      "$ref": "#/components/schemas/BulkUpdateResponse"
                    }
                  ]
                },
                "examples": {
                  "by_key": {
                    "summary": "Object body, with a warning (result_data shortened)",
                    "value": {
                      "success": true,
                      "result_text": "The contact was successfully updated.. 'Narnia' is not a valid value for country; field will not be updated",
                      "result_data": {
                        "id": "1002",
                        "email": "tenor.remote@example.com",
                        "remote_id": "HCS-0002",
                        "first_name": "Dan",
                        "country": ""
                      }
                    }
                  },
                  "bulk_all_succeeded": {
                    "summary": "List body, every contact updated",
                    "value": {
                      "success": true,
                      "successes": [
                        "1006",
                        "1007"
                      ],
                      "failures": [],
                      "result_data": {
                        "1006": {
                          "id": 1006,
                          "submit_type": "update",
                          "warnings": []
                        },
                        "1007": {
                          "id": 1007,
                          "submit_type": "update",
                          "warnings": []
                        }
                      },
                      "result_text": "All contacts were successfully updated"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Lookup failure, validation failure, unreadable body, or a list body in which any contact failed.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/PlainErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/FieldValidationErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/BulkUpdateResponse"
                    }
                  ]
                },
                "examples": {
                  "not_found_by_key": {
                    "summary": "No contact has the key value",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "No contact was found with the 'email': 'nobody@example.com'"
                    }
                  },
                  "multiple_by_key": {
                    "summary": "More than one contact has the key value",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "Multiple contacts were found with the 'remote_id': 'HCS-0002'.  (IDs: 1002, 1010)"
                    }
                  },
                  "key_field_missing": {
                    "summary": "key_field set but not in the body",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "Specified key_field 'remote_id' field was not provided."
                    }
                  },
                  "no_identifier": {
                    "summary": "No key_field, and neither id nor email in the body",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "An 'id' or 'email' (or alternate field specified by key_field query parameter) must be provided in order to update a contact"
                    }
                  },
                  "invalid_key_field": {
                    "summary": "key_field is not id, email, or remote_id",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "Invalid 'key_field' value: 'external_id'"
                    }
                  },
                  "key_field_id_mismatch": {
                    "summary": "Body id differs from the contact the key field found",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "The specified contact ID 1002 does not match record identified by key_field 'email': 'alto.lead@example.com' (ID: 1001)"
                    }
                  },
                  "body_id_not_found": {
                    "summary": "Body id without key_field does not exist",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "No contact was found with ID: '999999'"
                        ]
                      }
                    }
                  },
                  "bulk_partial_failure": {
                    "summary": "List body, one contact not found",
                    "value": {
                      "success": false,
                      "successes": [
                        "1006",
                        "1007"
                      ],
                      "failures": [
                        "CONTACT_OBJECT_2"
                      ],
                      "result_data": {
                        "1006": {
                          "id": 1006,
                          "submit_type": "update",
                          "warnings": []
                        },
                        "1007": {
                          "id": 1007,
                          "submit_type": "update",
                          "warnings": []
                        },
                        "CONTACT_OBJECT_2": {
                          "errors": [
                            "No contact was found with the 'email': 'missing.member@example.com'"
                          ]
                        }
                      },
                      "result_text": "2 contacts were successfully updated; 1 contacts were not updated"
                    }
                  },
                  "invalid_request": {
                    "summary": "Missing, empty, {}, or [] body",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "Invalid request"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/contacts/{id}": {
      "get": {
        "summary": "Get a single contact",
        "description": "Returns one contact as an object, in the same shape as an item of the list response. `fields` selects fields; filter, sort, and paging parameters are ignored. Returns an empty array (`[]`) if the ID is not found.",
        "operationId": "getContact",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContactId"
          },
          {
            "$ref": "#/components/parameters/Fields"
          }
        ],
        "responses": {
          "200": {
            "description": "The contact, or `[]` if not found.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Contact"
                    },
                    {
                      "type": "array",
                      "maxItems": 0
                    }
                  ]
                },
                "examples": {
                  "found": {
                    "summary": "Contact found",
                    "value": {
                      "id": "1001",
                      "external_id": "9f2c4e7a1b3d5f6a8c0e2b4d6f8a1c3e",
                      "date_created": "2026-10-05T14:03:11+00:00",
                      "remote_id": "",
                      "first_name": "Maya",
                      "last_name": "Okafor",
                      "title": "Section Lead",
                      "email": "alto.lead@example.com",
                      "company": "",
                      "phone": "416-555-0142",
                      "address_1": "48 Harbour Lane",
                      "address_2": "Unit 5",
                      "city": "Toronto",
                      "country": "CA",
                      "province": "ON",
                      "postal_code": "M5V 0Z9",
                      "website": "",
                      "language": "en",
                      "feedback": "",
                      "consent_status": "Express",
                      "consent_description": "Signed up at the spring concert",
                      "consent_effective_date": "2026-09-12T18:30:00+00:00",
                      "consent_expiry": "",
                      "custom_fields": {
                        "Voice_Part": "Alto",
                        "Season_Ticket_Number": "HCS-ST-0417"
                      },
                      "interests": [
                        "Volunteer",
                        "Alumni",
                        "Concert News"
                      ],
                      "autoresponders": []
                    }
                  },
                  "not_found": {
                    "summary": "Contact not found",
                    "value": []
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "summary": "Update a contact by ID",
        "description": "Updates the contact with the given ID and returns a short result: `result_data.id` (an integer here), `submit_type`, and `warnings` for unrecognised `country` or `language` values, plus `no_affected_rows: true` when no profile column changed, and `inapplicable_consent_updated` and `consent_fix_message` when an EU country change reset the contact's consent. `key_field` is ignored. If the body includes `id`, it must equal the URL id. A list body is treated as a bulk update (see `PATCH /contacts`) and the URL id is ignored. Preempted or blocked consent changes and skipped nurture campaigns are not reported.",
        "operationId": "updateContact",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContactId"
          },
          {
            "$ref": "#/components/parameters/WriteEmptyValues"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/ContactUpdate"
                  },
                  {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/ContactUpdate"
                    }
                  }
                ]
              },
              "examples": {
                "profile": {
                  "summary": "Change profile fields",
                  "value": {
                    "title": "Alto Section Lead",
                    "phone": "416-555-0199"
                  }
                },
                "clear_value": {
                  "summary": "Clear a value (?write_empty_values=1)",
                  "value": {
                    "address_2": ""
                  }
                },
                "clear_custom_field": {
                  "summary": "Clear a custom field (no write_empty_values needed)",
                  "value": {
                    "custom_fields": {
                      "Season_Ticket_Number": ""
                    }
                  }
                },
                "remove_tag": {
                  "summary": "Remove a tag, leaving others",
                  "value": {
                    "interests": {
                      "Alumni": "Unset"
                    }
                  }
                },
                "dequeue": {
                  "summary": "Stop a nurture campaign",
                  "value": {
                    "autoresponders": {
                      "Welcome Sequence": "Dequeue"
                    }
                  }
                },
                "force_downgrade": {
                  "summary": "Force a consent downgrade",
                  "value": {
                    "consent_status": "Implied - Transaction",
                    "consent_description": "Ticket purchase only; express consent withdrawn by phone",
                    "consent_force_downgrade": true
                  }
                },
                "eu_country": {
                  "summary": "Change country to an EU country",
                  "value": {
                    "country": "Germany"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contact updated (object body), or every contact updated (list body).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/UpdateByIdSuccessResponse"
                    },
                    {
                      "$ref": "#/components/schemas/BulkUpdateResponse"
                    }
                  ]
                },
                "examples": {
                  "updated": {
                    "summary": "Contact updated",
                    "value": {
                      "success": true,
                      "result_text": "The contact was successfully updated.",
                      "result_data": {
                        "id": 1001,
                        "submit_type": "update",
                        "warnings": []
                      }
                    }
                  },
                  "with_warning": {
                    "summary": "Unrecognised country",
                    "value": {
                      "success": true,
                      "result_text": "The contact was successfully updated.",
                      "result_data": {
                        "id": 1001,
                        "submit_type": "update",
                        "warnings": [
                          "'Narnia' is not a valid value for country; field will not be updated"
                        ]
                      }
                    }
                  },
                  "no_affected_rows": {
                    "summary": "Only a custom field changed",
                    "value": {
                      "success": true,
                      "result_text": "The contact was successfully updated.",
                      "result_data": {
                        "id": 1001,
                        "submit_type": "update",
                        "warnings": [],
                        "no_affected_rows": true
                      }
                    }
                  },
                  "eu_country_change": {
                    "summary": "Country changed to an EU country; implied consent reset",
                    "value": {
                      "success": true,
                      "result_text": "The contact was successfully updated. Their consent status was updated because Implied - Inquiry consent does not apply to contacts in the EU.",
                      "result_data": {
                        "id": 1004,
                        "submit_type": "update",
                        "inapplicable_consent_updated": true,
                        "consent_fix_message": " Their consent status was updated because Implied - Inquiry consent does not apply to contacts in the EU.",
                        "warnings": []
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not found, id mismatch, validation failure, field-validation failure, or unreadable body.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/FieldValidationErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/PlainErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/BulkUpdateResponse"
                    }
                  ]
                },
                "examples": {
                  "not_found": {
                    "summary": "No contact has this ID",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "No contact was found with ID: '999999'"
                        ]
                      }
                    }
                  },
                  "unknown_field": {
                    "summary": "Body has a key that is not a contact field",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "'nickname' is not a valid contact field"
                        ]
                      }
                    }
                  },
                  "field_validation": {
                    "summary": "Field too long (update envelope carries warnings)",
                    "value": {
                      "success": false,
                      "result_data": {
                        "fields": {
                          "fname": "First name must not exceed 50 characters"
                        },
                        "warnings": []
                      },
                      "result_text": "First name must not exceed 50 characters"
                    }
                  },
                  "email_empty": {
                    "summary": "Email cleared with write_empty_values=1",
                    "value": {
                      "success": false,
                      "result_data": {
                        "fields": {
                          "email": "Email must not be empty"
                        },
                        "warnings": []
                      },
                      "result_text": "Email must not be empty"
                    }
                  },
                  "id_mismatch": {
                    "summary": "Body id differs from the URL id",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "Contact 'id': 1001 does not match 'id' in request body (1002)"
                    }
                  },
                  "invalid_json": {
                    "summary": "Body is not valid JSON",
                    "value": {
                      "success": false,
                      "result_data": [],
                      "result_text": "Invalid JSON"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "summary": "Delete a contact",
        "description": "Permanently deletes the contact: its record, consent history, custom field values, tag, subscription, and mandatory consent memberships, nurture campaign memberships and queued campaign actions, and form submissions. The ID is never reused. A contact whose consent status is `Revoked`, `Envoke Spam Reported`, or `External Spam Reported` cannot be deleted. Responses have only `success` and `result_text`. If the delete fails on the server, `result_text` is `1 contact not deleted, 1 - delete error: ` followed by the error detail.",
        "operationId": "deleteContact",
        "tags": [
          "Contacts"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContactId"
          }
        ],
        "responses": {
          "200": {
            "description": "Contact deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteResponse"
                },
                "example": {
                  "success": true,
                  "result_text": "1 contact deleted"
                }
              }
            }
          },
          "400": {
            "description": "Not found, blocked by consent status, or a server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteResponse"
                },
                "examples": {
                  "not_found": {
                    "summary": "No contact has this ID",
                    "value": {
                      "success": false,
                      "result_text": "1 contact not deleted, 1 - no matching contact found"
                    }
                  },
                  "blocked": {
                    "summary": "Consent status blocks deletion",
                    "value": {
                      "success": false,
                      "result_text": "1 contact not deleted, 1 - can't delete due to consent status"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  }
}
