openapi: 3.0.0
info:
    version: 1.1.0
    title: 'Qomon Public API Documentation'
    description: 'Qomon Public API — Contacts, Search, and Forms.'
servers:
    -
        url: '{{SERVER_URL}}'
security:
    -
        bearerAuth: []
tags:
    -
        name: Contacts
        description: 'Create, Read, Update, Delete Qomon''s contacts.'
    -
        name: Forms
        description: 'Retrieve forms by their ID or list them based on their type.'
paths:
    /contacts/upsert:
        post:
            tags: [Contacts]
            summary: 'Upsert a contact asynchronously. (Strongly recommended)'
            description: "Upsert a contact asynchronously in the Qomon database.\n\n\nWhen upserting, the system tries to find an existing contact to update using the following strategy (in order). If no match is found, a new contact is created.\n\n**1. Match by ID**\n  - If an `id` is provided and matches an existing contact, that contact will be updated (can be a contact ID or nationbuilder ID).\n\n**2. Match by email + name**\n  - If no ID is provided (or it didn't match), the system searches for contacts with the same `mail`.\n  - Among the results, if `firstname` is provided, it must also match. If `surname` is provided, it must also match. (An empty value for `firstname` or `surname` means the field will not be used for matching.)\n  - If a match is found, that contact is updated.\n\n**3. Match by surname + firstname + birthdate + address**\n  - If no match was found by email, and a `surname` is provided, the system searches for contacts with the same `surname`.\n  - Among the results, `firstname` must match.\n  - If both the incoming and the existing contact have a `birthdate`, they must match exactly.\n  - If both contacts have an `address`, the `city` or `postalcode` must match. If no birthdate comparison was made, the `street` must also match.\n  - If all the above conditions are met, that contact is updated.\n\n**4. No match found**\n  - If none of the above strategies found a match, a new contact is created.\n\n> ⚠️ If the `id` or `label` of an advanced field, or the `value` (for non-free-text fields), does not exist, the contact will not be updated or created. **Errors will not be logged.**\n\n💡 For consents, level_of_support, custom_fields, survey, tasks:\n  - See details in above description.\n  - Found `id`, `label`, `value` in GET /forms/{type} endpoint."
            operationId: upsertContact
            requestBody: {content: {application/json: {schema: {type: object, required: [kind, data], additionalProperties: false, properties: {kind: {type: string, enum: [contact]}, data: {$ref: '#/components/schemas/ContactUpsert'}}}, examples: {'Basic fields': {value: {kind: contact, data: {firstname: John, surname: Doe, gender: M, mail: john.doe@example.com, married_name: Doe, birthdate: '1999-12-31T23:00:00.000Z', birthcity: Paris, birthcountry: France, nationality: Francais, mobile: '0123456789', phone: '0123456789', address: {building: A, floor: '2', door: '3', housenumber: '123', street: 'Rue de la paix', addition: 'Résidence de la paix', postalcode: '75001', city: Paris, country: France}, tags: [{name: tag1}, {name: tag2}]}}}, 'Advanced fields (survey, custom fields, consent)': {value: {kind: contact, data: {firstname: John, surname: Doe, gender: M, mail: john.doe@example.com, married_name: Doe, birthdate: '1999-12-31T23:00:00.000Z', birthcity: Paris, birthcountry: France, mobile: '0123456789', phone: '0123456789', address: {building: A, floor: '2', door: '3', housenumber: '123', street: 'Rue de la paix', addition: 'Résidence de la paix', postalcode: '75001', city: Paris, country: France}, tags: [{name: tag1}, {name: tag2}], custom_fields: [{label: Job, value: Architect}, {label: 'Favorites food', value: hamburger}, {label: 'Favorites food', value: pizza}], status: [{label: 'Level of support', value: to_be_convinced}], consents: [{label: 'The contact authorises me to collect and use the collected data for my', value: yes}], forms: [{label: 'What do you see as the city''s strengths?', value: Roads}], actions: [{label: 'Next step', value: recevoir_nos_informations}]}}}, 'Basic fields with email consent': {value: {kind: contact, data: {firstname: John, surname: Doe, gender: M, mail: john.doe@example.com, married_name: Doe, birthdate: '1999-12-31T23:00:00.000Z', birthcity: Paris, birthcountry: France, mobile: '0123456789', phone: '0123456789', address: {building: A, floor: '2', door: '3', housenumber: '123', street: 'Rue de la paix', addition: 'Résidence de la paix', postalcode: '75001', city: Paris, country: France}, consents: {value: consent_email}}}}}}}}
            responses: {'202': {description: Accepted}, '400': {description: 'Invalid input'}, '500': {description: 'Internal server error'}}
    '/contacts/{contactId}':
        get:
            tags: [Contacts]
            summary: 'Find a contact by ID.'
            description: "Returns a single contact. Use this endpoint to get a contact by its ID.\n\nIn the response advanced fields except custom fields are formatted as `formdatas` structure with `form_id` and `form_ref_id`.\n  - `form_id`: the ID of the advanced field\n  - `form_ref_id`: the ID of the value of the advanced field\n\n  ```\n  \"formdatas\": [\n    {\n      \"form_id\": 47566,\n      \"form_ref_id\": 15648,\n      \"data\": \"consent_email\"\n    }\n  ]\n  ```"
            operationId: getContactPerId
            parameters: [{name: contactId, in: path, description: 'ID of contact to return', required: true, schema: {type: integer}}]
            responses: {'200': {description: 'Successful operation', content: {application/json: {schema: {type: object, properties: {status: {type: string, example: success}, data: {type: object, properties: {contact: {$ref: '#/components/schemas/ContactResponse'}}}}}}}}, default: {description: 'Unsuccessful operation', content: {application/json: {schema: {type: object, properties: {status: {type: string, example: fail}}}}}}}
        delete:
            tags: [Contacts]
            summary: 'Delete a contact by ID synchronously.'
            operationId: deleteContactPerId
            parameters: [{name: contactId, in: path, description: 'ID of contact to delete', required: true, schema: {type: integer}}]
            responses: {'204': {description: 'Successful operation'}}
        patch:
            tags: [Contacts]
            summary: 'Fully update (REPLACE) an existing contact by id synchronously.'
            description: "Update entirely an existing contact by id synchronously in the Qomon database (behaves like PUT).\nFully replace an existing contact in the Qomon database.\n\n⚠️ Important:\n- This endpoint uses the PATCH verb but behaves like a PUT (full replacement).\n- Any fields that are NOT included in the payload will be cleared/removed.\n- Always send the full contact object if you don't want to lose existing data.\n\nTo synchronize and mass update your contacts with Qomon database, use the UPSERT route instead.\n\nTo specify the advanced fields (survey, consents, etc), you need to use the `formdatas` structure with `form_id` and `form_ref_id`.\n- `form_id`: the ID of the advanced field\n- `form_ref_id`: the ID of the value of the advanced field\n- `data`: free text if the field (\"forms\") type is Text, Integer, or Date (input format: `YYYY-MM-DD'T'HH:mm:ss.SSS'Z'`; responses may return up to microsecond precision: `YYYY-MM-DD'T'HH:mm:ss.SSSSSS'Z'`); otherwise, use the value of the refvalue\n- `date`: the date of the formdata entry\n\n```\n\"formdatas\": [\n  {\n    \"form_id\": 47566,\n    \"form_ref_id\": 15648,\n    \"data\": \"consent_email\"\n  }\n]\n```\nTo specify the custom fields, you need to use the `custom_fields` structure with `form_id` and `form_ref_id`.\n- `form_id`: the ID of the custom field\n- `form_ref_id`: the ID of the value of the custom field\n- `data`: free text if the field (\"forms\") type is Text, Integer, or Date (input format: `YYYY-MM-DD'T'HH:mm:ss.SSS'Z'`; responses may return up to microsecond precision: `YYYY-MM-DD'T'HH:mm:ss.SSSSSS'Z'`); otherwise, use the value of the refvalue\n- `date`: the date of the custom_field entry \n\n```\n\"custom_fields\": [\n  {\n    \"form_id\": 879854,\n    \"form_ref_id\": 98741,\n    \"data\": \"hamburger\",\n    \"date\": \"2024-07-17T11:34:42.195Z\"\n  }\n]\n```\nNB :\n  - Each patch will generate new ids for formdatas and custom_fields. (due to the full replace behavior : soft delete + recreate)\n  - If you don't send the exhaustive list of formdatas or custom_fields, the missing ones will be deleted (soft).\n  - For Text, Integer and Date Fields, If you add a formdata or custom_field, it will be added to the existing ones.\n  - For Radio (single select), If you add a formdata or custom_field, it will replace the existing ones.\n  - For Checkbox (multi-select) fields, any new value will be added to the existing ones.\n  - If you change a formdata or custom_field with, it will update it. (the id is not required here because of the full-replace behavior)."
            operationId: updateContactPerId
            parameters: [{name: contactId, in: path, description: 'ID of contact to update', required: true, schema: {type: integer}}]
            requestBody: {description: 'The new values of the contact', required: true, content: {application/json: {schema: {type: object, properties: {data: {type: object, properties: {contact: {$ref: '#/components/schemas/Contact'}}}}}}}}
            responses: {'200': {description: 'Successful operation', content: {application/json: {schema: {type: object, properties: {status: {type: string, example: success}, data: {type: object, properties: {contact: {$ref: '#/components/schemas/ContactResponse'}}}}}}}}, default: {description: 'Unsuccessful operation', content: {application/json: {schema: {type: object, properties: {status: {type: string, example: fail}}}}}}}
    /contacts:
        post:
            tags: [Contacts]
            summary: 'Create a contact synchronously.'
            description: "Create a contact synchronously in the Qomon database.\n\n> To synchronize and mass update your contacts, use the UPSERT route instead.\n\nTo specify the advanced (custom fields, survey, etc) fields, you need to use the `formdatas` structure with `form_id` and `form_ref_id`.\n- `form_id`: the ID of the advanced field\n- `form_ref_id`: the ID of the value of the advanced field\n\n```\n\"formdatas\": [\n  {\n    \"form_id\": 47566,\n    \"form_ref_id\": 15648,\n    \"data\": \"consent_email\"\n  }\n]\n```"
            operationId: createContact
            requestBody: {required: true, content: {application/json: {schema: {type: object, required: [data], properties: {data: {type: object, required: [contact], properties: {contact: {$ref: '#/components/schemas/Contact'}}}}}, examples: {Basic: {value: {data: {contact: {firstname: John, surname: Doe, birthdate: '1999-12-31T23:00:00.000Z', mail: john.doe@example.com, mobile: '0123456789', address: {building: A, floor: '2', door: '3', housenumber: '123', street: 'Rue de la paix', addition: 'Résidence de la paix', postalcode: '75001', city: Paris, country: France}}}}}, 'Basic with formdatas': {value: {data: {contact: {gender: M, firstname: John, surname: Doe, married_name: Doe, birthdate: '1999-12-31T23:00:00.000Z', birthcity: Paris, birthcountry: France, nationality: Francais, mail: john.doe@example.com, mobile: '0123456789', phone: '0123456789', address: {building: A, floor: '2', door: '3', housenumber: '123', street: 'Rue de la paix', addition: 'Résidence de la paix', postalcode: '75001', city: Paris, country: France}, tags: [{name: tag1}, {name: tag2}], formdatas: [{form_id: 47566, form_ref_id: 15648, data: consent_email}, {form_id: 879856, form_ref_id: 98746, data: Yes}], custom_fields: [{form_id: 879854, form_ref_id: 98741, data: hamburger}], black_list: false}}}}}}}}
            responses: {'200': {description: 'Successful operation', content: {application/json: {schema: {type: object, required: [status, data], properties: {status: {type: string, example: success}, data: {type: object, required: [contact], properties: {contact: {$ref: '#/components/schemas/ContactResponse'}}}}}}}}, default: {description: 'Unsuccessful operation', content: {application/json: {schema: {type: object, properties: {status: {type: string, example: fail}}}}}}}
    /search:
        post:
            tags: [Contacts]
            summary: 'Search for contacts'
            description: "Search contacts using a flexible query language. Filter by any combination of contact info, address, tags, financial activity (transactions, donations, memberships), email campaign engagement, and custom fields.\n\n---\n\n\nEach condition uses an `ope` (operator). Here is what they mean in plain language:\n\n| What you want to say | `ope` value |\n|----------------------|-------------|\n| equals | `eql` |\n| does not equal | `not_eql` |\n| has a value / is filled in | `ext` |\n| has no value / is empty | `not_ext` |\n| is before / at most | `lte` |\n| is after / at least | `gte` |\n| is between two values | `range` — use with `from` and `to` |\n| contains the text | `contains` |\n| does not contain the text | `not_contains` |\n| starts with | `start_with` |\n| does not start with | `not_start_with` |\n| equals (case-sensitive, exact match) | `eql:strictdata` |\n| matches any of a list | `any_of` |\n| matches none of a list | `none_of` |\n| matches all of a list | `all_of` |\n\n> Dates accept relative values: `\"now\"`, `\"now-30d\"`, `\"now-1y\"`, etc.\n\n---\n\n\nA query is a **tree of groups**. Each group is either:\n- **`$all`** — ALL conditions inside must match (AND logic)\n- **`$at_least_one`** — AT LEAST ONE condition must match (OR logic)\n\nGroups can be nested one level deep. Think of it as:\n\n```\nquery  ($all = the outer AND)\n ├── group A  ($all — all of these must match)\n │    ├── city equals \"Paris\"\n │    └── mobile has a value\n └── group B  ($at_least_one — at least one must match)\n      ├── tag equals \"volunteer\"\n      └── tag equals \"donor\"\n```\n\nPlain English: *\"City is Paris AND has a mobile number AND (tagged volunteer OR tagged donor)\"*\n\nTranslated to JSON:\n```json\n{\n  \"data\": {\n    \"advanced_search\": {\n      \"per_page\": 100,\n      \"query\": {\n        \"$all\": [\n          {\n            \"$all\": [\n              { \"$condition\": { \"attr\": \"address.city\", \"ope\": \"eql\", \"value\": \"Paris\" } },\n              { \"$condition\": { \"attr\": \"mobile\", \"ope\": \"ext\" } }\n            ]\n          },\n          {\n            \"$at_least_one\": [\n              { \"$condition\": { \"attr\": \"tags.name\", \"ope\": \"eql\", \"value\": \"volunteer\" } },\n              { \"$condition\": { \"attr\": \"tags.name\", \"ope\": \"eql\", \"value\": \"donor\" } }\n            ]\n          }\n        ]\n      }\n    }\n  }\n}\n```\n\n> **Single condition?** You still need the two-level nesting. The minimum shape is always:\n> `{ \"$all\": [ { \"$all\": [ { \"$condition\": { ... } } ] } ] }`\n\n---\n\n\n*Contacts whose membership started on or before today and ends on or after today.*\n```json\n{\n  \"data\": {\n    \"advanced_search\": {\n      \"per_page\": 100,\n      \"query\": {\n        \"$all\": [{\n          \"$all\": [\n            { \"$condition\": { \"attr\": \"memberships.start_date\", \"ope\": \"lte\", \"value\": \"now\" } },\n            { \"$condition\": { \"attr\": \"memberships.end_date\",   \"ope\": \"gte\", \"value\": \"now\" } }\n          ]\n        }]\n      }\n    }\n  }\n}\n```\n\n*Contacts whose cumulative donation amount is at least €100. Amount values are always passed as strings in euros.*\n```json\n{\n  \"data\": {\n    \"advanced_search\": {\n      \"per_page\": 100,\n      \"query\": {\n        \"$all\": [{\n          \"$all\": [\n            { \"$condition\": { \"attr\": \"donations.sum_amount\", \"ope\": \"gte\", \"value\": \"100\" } }\n          ]\n        }]\n      }\n    }\n  }\n}\n```\n\n*Has a delivered email for campaign 42, but no open timestamp.*\n```json\n{\n  \"data\": {\n    \"advanced_search\": {\n      \"per_page\": 100,\n      \"query\": {\n        \"$all\": [{\n          \"$all\": [\n            { \"$condition\": { \"attr\": \"emails.campaign_id\",  \"ope\": \"eql\",     \"value\": \"42\" } },\n            { \"$condition\": { \"attr\": \"emails.delivered_at\", \"ope\": \"ext\" } },\n            { \"$condition\": { \"attr\": \"emails.opened_at\",    \"ope\": \"not_ext\" } }\n          ]\n        }]\n      }\n    }\n  }\n}\n```\n\n*Combining two nested field groups — each is an independent AND.*\n```json\n{\n  \"data\": {\n    \"advanced_search\": {\n      \"per_page\": 100,\n      \"query\": {\n        \"$all\": [\n          {\n            \"$all\": [\n              { \"$condition\": { \"attr\": \"memberships.period\", \"ope\": \"eql\", \"value\": \"2024\" } }\n            ]\n          },\n          {\n            \"$all\": [\n              { \"$condition\": { \"attr\": \"donations.count_amount\", \"ope\": \"gte\", \"value\": \"1\" } }\n            ]\n          }\n        ]\n      }\n    }\n  }\n}\n```\n\n*An OR group for cities combined with an AND group for the mobile requirement.*\n```json\n{\n  \"data\": {\n    \"advanced_search\": {\n      \"per_page\": 100,\n      \"query\": {\n        \"$all\": [\n          {\n            \"$at_least_one\": [\n              { \"$condition\": { \"attr\": \"address.city\", \"ope\": \"eql\", \"value\": \"Paris\" } },\n              { \"$condition\": { \"attr\": \"address.city\", \"ope\": \"eql\", \"value\": \"Lyon\" } }\n            ]\n          },\n          {\n            \"$all\": [\n              { \"$condition\": { \"attr\": \"mobile\", \"ope\": \"ext\" } }\n            ]\n          }\n        ]\n      }\n    }\n  }\n}\n```\n\n*Use `GET /forms/{type}` to find your `form_id`, then `GET /forms/{formId}` for `form_ref_ids`.*\n```json\n{\n  \"data\": {\n    \"advanced_search\": {\n      \"per_page\": 100,\n      \"query\": {\n        \"$all\": [{\n          \"$all\": [{\n            \"$condition\": {\n              \"attr\": \"form\",\n              \"form_id\": 4530,\n              \"form_ref_ids\": [7486],\n              \"ope\": \"eql\",\n              \"value\": \"consent_email\"\n            }\n          }]\n        }]\n      }\n    }\n  }\n}\n```\n\n---\n\n\n\n| Attribute | Meaning | Operators |\n|-----------|---------|-----------|\n| `firstname` | First name | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |\n| `surname` | Last name | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |\n| `married_name` | Married name | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |\n| `gender` | Gender (`M`, `F`, `O`) | `eql`, `not_eql`, `ext`, `not_ext` |\n| `birthdate` | Date of birth | `eql`, `ext`, `not_ext`, `range`, `lte`, `gte`, `period` |\n| `age_cat` | Age category (0–6) | `eql`, `not_eql`, `ext`, `not_ext` |\n| `birthcity` | City of birth | `eql`, `not_eql`, `ext`, `not_ext` |\n| `birthcountry` | Country of birth | `eql`, `not_eql`, `ext`, `not_ext` |\n| `nationality` | Nationality | `eql`, `not_eql`, `ext`, `not_ext` |\n| `mail` | Email address | `eql:strictdata`, `not_eql:strictdata`, `ext`, `not_ext`, `start_with`, `not_start_with` |\n| `phone` | Landline | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |\n| `mobile` | Mobile number | `eql`, `not_eql`, `ext`, `not_ext`, `contains`, `start_with`, `not_start_with` |\n| `tags.name` | Tag name | `eql`, `not_eql`, `any_of`, `none_of`, `all_of`, `ext`, `not_ext` |\n| `black_list` | Blocked from all communication | `eql`, `not_eql` |\n| `lastchange` | Last modification date | `eql`, `ext`, `not_ext`, `range`, `lte`, `gte` |\n| `user_id` | ID of last user who edited the contact | `eql`, `not_eql`, `ext`, `not_ext` |\n| `user_contact_id` | ID of the linked Qomon user | `ext`, `not_ext` |\n| `action_ids` | IDs of linked actions | `eql`, `not_eql` |\n\n\n| Attribute | Meaning | Operators |\n|-----------|---------|-----------|\n| `address.city` | City | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |\n| `address.postalcode` | Postal code | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |\n| `address.country` | Country | `eql`, `not_eql`, `ext`, `not_ext` |\n| `address.street` | Street name | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |\n| `address.housenumber` | House number | `eql`, `not_eql`, `ext`, `not_ext` |\n| `address.building` | Building | `eql`, `not_eql`, `ext`, `not_ext` |\n| `address.floor` | Floor | `eql`, `not_eql`, `ext`, `not_ext` |\n| `address.door` | Door | `eql`, `not_eql`, `ext`, `not_ext` |\n| `address.addition` | Address complement | `eql`, `not_eql`, `ext`, `not_ext` |\n| `address.pollingstation` | Polling station | `eql`, `not_eql`, `any_of`, `none_of`, `ext`, `not_ext` |\n| `address.invalid` | Address flagged as invalid | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |\n\n\n> These fields match contacts who have **at least one** matching record. `transactions.amount gte \"50\"` returns contacts with at least one transaction ≥ €50, not contacts whose total is ≥ €50 (use `sum_amount` for that).\n>\n> **Amount fields** (`*.amount`, `*.sum_amount`) are in euros — always pass as strings: `\"value\": \"50\"` not `\"value\": 50`. `sum_amount` is the total across all records for the contact; `count_amount` is the number of records.\n\n**Transactions** (`transactions.*`)\n\n| Attribute | Meaning | Operators |\n|-----------|---------|-----------|\n| `transactions.created_at` | Record creation date | `lte`, `gte`, `range` |\n| `transactions.date` | Transaction execution date | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `transactions.amount` | Amount of one transaction (€) | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `transactions.sum_amount` | Total amount across all transactions (€) | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `transactions.count_amount` | Number of transactions | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `transactions.code_campaign` | Campaign code | `ext`, `not_ext`, `eql`, `not_eql`, `contains`, `not_contains`, `start_with`, `not_start_with` |\n| `transactions.payment_method_kind` | Payment method | `eql`, `not_eql` |\n| `transactions.status_id` | Transaction status ID | `eql`, `not_eql` |\n\n**Donations** (`donations.*`)\n\n| Attribute | Meaning | Operators |\n|-----------|---------|-----------|\n| `donations.created_at` | Record creation date | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `donations.transaction_date` | Donation operation date | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `donations.amount` | Amount of one donation (€) | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `donations.sum_amount` | Total donated (€) | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `donations.count_amount` | Number of donations | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `donations.donation_price_id` | Donation type/tier ID | `eql`, `not_eql` |\n\n**Memberships** (`memberships.*`)\n\n| Attribute | Meaning | Operators |\n|-----------|---------|-----------|\n| `memberships.created_at` | Record creation date | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `memberships.start_date` | Membership start date | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `memberships.end_date` | Membership end date | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `memberships.transaction_date` | Membership operation date | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `memberships.period` | Active year — e.g. `\"2024\"` means active that calendar year | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `memberships.multiple_in_period` | Had more than one membership in a given year | `eql` (value = year string e.g. `\"2024\"`) |\n| `memberships.membership_price_id` | Membership type/tier ID | `eql`, `not_eql` |\n\n\n> These match contacts who have at least one email record where that field is set. A contact with no email history at all will not match `ext` or `not_ext`.\n\n| Attribute | Meaning | Operators |\n|-----------|---------|-----------|\n| `emails.created_at` | Email record date | `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `emails.type` | Email type | `ext`, `not_ext`, `eql`, `not_eql` |\n| `emails.sender` | Sender address | `ext`, `not_ext`, `eql`, `not_eql` |\n| `emails.subject` | Email subject line | `ext`, `not_ext`, `contains`, `not_contains`, `start_with`, `not_start_with` |\n| `emails.user_id` | ID of Qomon user who sent the campaign | `ext`, `not_ext`, `eql`, `not_eql` |\n| `emails.campaign_id` | Campaign ID | `ext`, `not_ext`, `eql`, `not_eql` |\n| `emails.campaign_name` | Campaign name | `ext`, `not_ext`, `contains`, `not_contains`, `start_with`, `not_start_with` |\n| `emails.campaign_goal` | Campaign goal | `ext`, `not_ext`, `eql`, `not_eql` |\n| `emails.campaign_is_successful` | Campaign marked successful | `ext`, `not_ext`, `eql`, `not_eql` |\n| `emails.campaign_performance_rating` | Campaign performance score (0–100) | `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `emails.template_id` | Template ID | `ext`, `not_ext`, `eql`, `not_eql` |\n| `emails.template_name` | Template name | `ext`, `not_ext`, `contains`, `not_contains`, `start_with`, `not_start_with` |\n| `emails.delivered_at` | Email delivered date | `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `emails.opened_at` | Email opened date | `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `emails.clicked_at` | Email clicked date | `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `emails.bounced_at` | Email bounced date | `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `emails.unsubscribed_at` | Unsubscribed date | `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `emails.blacklisted_at` | Blacklisted date | `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `emails.abuse_report_at` | Abuse report date | `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `emails.clicked_urls` | A URL that was clicked | `ext`, `not_ext`, `eql`, `not_eql`, `contains`, `start_with` |\n\n\n`sanctions.validity` is a virtual computed field — it does not store a date, it tells you whether a sanction is currently active:\n- `\"ongoing\"` — sanction has started and has not yet ended\n- `\"ended\"` — sanction end date is in the past\n\n| Attribute | Meaning | Operators |\n|-----------|---------|-----------|\n| `sanctions.created_at` | Sanction creation date | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `sanctions.start_date` | Sanction start date | `eql`, `not_eql`, `lte`, `gte`, `range` |\n| `sanctions.end_date` | Sanction end date | `eql`, `not_eql`, `ext`, `not_ext`, `lte`, `gte`, `range` |\n| `sanctions.status` | Sanction status | `eql`, `not_eql`, `contains`, `not_contains` |\n| `sanctions.validity` | `\"ongoing\"` or `\"ended\"` | `eql` |\n\n\nThese two attributes use a **different condition shape** — you must also provide:\n- `form_id` — the ID of the form (find it via `GET /forms/{type}`)\n- `form_ref_ids` — array of answer option IDs to match (find them via `GET /forms/{formId}`)\n\n| Attribute | What it searches | Operators |\n|-----------|-----------------|-----------|\n| `custom_fields` | Custom fields attached to contacts | `ext`, `not_ext`, `eql`, `not_eql`, `contains`, `not_contains`, `lte`, `gte`, `range` |\n| `form` | Survey, consent, level of support, presence, or task form answers | `ext`, `not_ext`, `eql`, `not_eql`, `contains`, `not_contains`, `lte`, `gte`, `range` |\n\n> `ext` / `not_ext` only check whether the contact has filled in the given `form_id` — `form_ref_ids` is ignored.\n> `eql` without a `value` checks that all listed `form_ref_ids` are present on the contact.\n> `eql` with a `value` checks that they are present AND that `value` matches the stored answer.\n\n\n| Parameter | Where | Description |\n|-----------|-------|-------------|\n| `per_page` | `advanced_search` | Contacts per page — **required**, 1–1000 |\n| `page` | `advanced_search` | Page number (default: 0) |\n| `sort_attr` | `advanced_search` | Sort by: `surname`, `firstname`, `birthdate`, `gender`, `lastchange`, `mail`, `married_name`, `city` |\n| `sort_asc` | `advanced_search` | `true` = ascending order (default: false) |\n| `include_interactions` | `advanced_search` | `true` = include each contact's interaction history in the response |\n"
            requestBody: {required: true, content: {application/json: {schema: {type: object, required: [data], properties: {data: {$ref: '#/components/schemas/Search'}}}, examples: {'Minimal Search': {$ref: '#/components/examples/minimalSearch'}, 'Search example 1': {$ref: '#/components/examples/advancedSearchSimple'}, 'Advanced example 2 (form/consent)': {$ref: '#/components/examples/advancedSearchFull'}, 'Search by total transaction amount': {$ref: '#/components/examples/searchByTransaction'}, 'Search by donation in period': {$ref: '#/components/examples/searchByDonation'}, 'Search by active membership': {$ref: '#/components/examples/searchByMembership'}, 'Search by email campaign engagement': {$ref: '#/components/examples/searchByEmailCampaign'}}}}}
            responses: {'200': {description: 'Successful operation', content: {application/json: {schema: {type: object, properties: {status: {type: string, example: success}, data: {type: object, properties: {contacts: {type: array, items: {$ref: '#/components/schemas/ContactResponseWithInteractions'}}}}}, example: {status: success, data: {contacts: [{id: 12345678, group_id: 1, CreatedAt: '2022-10-13T15:50:34.643724Z', UpdatedAt: '2022-10-13T15:55:12.853724Z', lastchange: '2022-10-13T15:50:24.163Z', firstname: John, surname: Doe, married_name: Smith, gender: F, birthdate: '1955-07-06T00:00:00Z', age_category: 0, birthdept: string, birthcity: string, birthcountry: string, mail: user@example.com, phone: string, mobile: string, address: {housenumber: 1, street: 'rue de la paix', postalcode: 75000, citycode: 75101, city: Paris, state: Ile-de-France, country: France, addition: 'Résidence de la paix', infos: 'Porte de gauche', building: 'Bâtiment A', floor: 2, door: A, pollingstation: BV-06, id: 1001, score: 0.965753, latitude: 48, longitude: 2, location: 48.0000002.000000, invalid: null}, tags: [{name: tag1, color: red}], actions_ids: [1, 2], formdatas: [{form_id: 1001, form_ref_id: 2001, data: Oui, id: 3001, created_at: '2022-10-13T15:50:24.163Z', updated_at: '2022-10-13T15:50:24.163Z', date: '2022-10-13T15:50:24.163Z', group_id: 1, contact_id: 12345678}], customfields: [{form_id: 1001, form_ref_id: 2001, data: Oui, id: 3001, created_at: '2022-10-13T15:50:24.163Z', updated_at: '2022-10-13T15:50:24.163Z', date: '2022-10-13T15:50:24.163Z', group_id: 1, contact_id: 12345678}], notes: {id: 1234, created_at: '2024-09-12T08:06:47.957Z', updated_at: '2024-09-12T08:06:47.957Z', data: 'John Doe asked for information about the next event.', contact_id: 12345678, group_id: 1, user_id: 456, pinned: true}, interactions: [{id: 0, created_at: '2024-09-12T08:06:47.957Z', updated_at: '2024-09-12T08:06:47.957Z', type: letter, sub_type: incoming, title: 'Send a request for information', comments: 'John Doe asked for information about the next event.', contact_id: 12345678, group_id: 1, user_id: 456, only_super_admin: false, files: [{id: 5001, created: '2024-09-12T08:06:47.957Z', owner_id: 456, group_id: 1, subject: 'The letter sent by John Doe', url: string, folder: string, mime: image/jpeg, name: string, is_interaction: true}]}]}]}}}}}}, default: {description: 'Unsuccessful operation'}}
    /kpi:
        post:
            tags: [Contacts]
            summary: 'Get KPIs about contacts'
            description: 'Returns the count of contacts matching a query. Same query format as `/search` but only returns `{"data": {"total": N}}`. `per_page` is not required and is ignored.'
            requestBody: {content: {application/json: {schema: {type: object, required: [data], properties: {data: {$ref: '#/components/schemas/KpiSearch'}}}, examples: {'Minimal Search': {$ref: '#/components/examples/minimalSearch'}, 'Search example 1': {$ref: '#/components/examples/advancedSearchSimple'}, 'Advanced example 2 (form/consent)': {$ref: '#/components/examples/advancedSearchFull'}, 'Search by total transaction amount': {$ref: '#/components/examples/searchByTransaction'}, 'Search by donation in period': {$ref: '#/components/examples/searchByDonation'}, 'Search by active membership': {$ref: '#/components/examples/searchByMembership'}, 'Search by email campaign engagement': {$ref: '#/components/examples/searchByEmailCampaign'}}}}}
            responses: {'200': {description: 'Successful operation', content: {application/json: {schema: {type: object, properties: {status: {type: string, example: success}, data: {type: object, properties: {total: {type: integer, example: 1597}}}}}}}}}
    '/forms/{formId}':
        get:
            tags: [Forms]
            summary: 'Retrieve a form using its ID.'
            description: "Retrieve detailed information about a specific form, including its label, type, and possible refvalues.\n\nA form represents a field or a question that can be associated with a contact. It contains:\n- Label: The name or description of the form (e.g., \"What is your preferred contact method?\").\n- Type: The format of the form, such as radio, text, checkbox, numeric, date, range, signature or photos\n- Refvalues: A list of possible answers or predefined values associated with the form. These can be simple options (e.g., \"Yes\" or \"No\") or a range of values, depending on the form type.\n\nHere’s an example of a form with a range type, where the refvalue specifies a minimum, maximum, and step for the range of answers:\n\n```\n{\n  \"id\": 12515,\n  \"type\": \"range\",\n  \"section\": \"My city and me\",\n  \"order\": 1,\n  \"label\": \"On a scale of 1 to 10, how would you rate your satisfaction with the council?\",\n  \"refvalues\": [\n    {\n      \"id\": 19477,\n      \"form_id\": 12515,\n      \"min\": 0,\n      \"max\": 10,\n      \"step\": 1\n    }\n  ]\n}\n```\n\nThis allows the form to represent a sliding scale, and the response can specify any value within this range.\n\n**Link with contacts: formdatas & custom_fields**\n\nWhen retrieving or working with contacts, the formdatas field links forms and refvalues to a contact. Custom fields have a dedicated structure in the contact object.\n\nEach entry in formdatas and custom_fields contains:\n- form_id: The ID of the form.\n- form_ref_id: The ID of the refvalue selected for that form.\n- data: The actual value or answer provided (e.g., \"7\" for a rating of 7 on the scale from 0 to 10).\n\nExample of formdatas in a contact for the range form:\n\n```\n\"formdatas\": [\n  {\n    \"form_id\": 12515,\n    \"form_ref_id\": 19477,\n    \"data\": \"7\"\n  }\n],\n\"custom_fields\": [\n  {\n    \"form_id\": 11391,\n    \"form_ref_id\": 18151\n    \"data\": \"Architect\",\n  }\n]\n```"
            parameters: [{name: formId, in: path, description: 'ID of the form to return', required: true, schema: {type: integer}}]
            responses: {'200': {description: 'Successful operation', content: {application/json: {schema: {type: object, properties: {status: {type: string, example: success}, data: {type: object, properties: {form: {$ref: '#/components/schemas/Form'}}}}}}}}}
    '/forms/{type}':
        get:
            tags: [Forms]
            summary: 'Retrieve forms by their type.'
            description: "See all forms of a specific type.\n\nThe type parameter allows you to filter forms by their type. The available types are:\n- consent: Forms related to consent and data collection.\n- level_of_support: Forms related to the level of support provided.\n- presence_status: Forms related to the presence status of the contact.\n- custom_fields: Custom fields associated with the contact.\n- survey: Forms related to surveys and questionnaires.\n- tasks: Forms related to tasks and actions.\n\nMost forms can be viewed and managed through the Qomon interface in the space settings."
            parameters: [{name: type, in: path, description: 'Type of form to return', required: true, schema: {type: string, enum: [consent, level_of_support, presence_status, custom_fields, survey, tasks]}}]
            responses: {'200': {description: 'Successful operation', content: {application/json: {schema: {type: object, properties: {status: {type: string, example: success}, data: {type: object, properties: {forms: {type: array, items: {$ref: '#/components/schemas/Form'}}}}}}, examples: {Consent: {value: {data: {forms: [{id: 11240, group_id: [631], CreatedAt: '2024-01-05T10:12:56.029114Z', UpdatedAt: '2024-01-05T10:12:56.029114Z', label: "The contact authorises me to collect and use the collected data for my\naction", type: checkbox, refvalues: [{id: 17872, form_id: 11240, CreatedAt: '2024-01-05T10:12:56.029922Z', UpdatedAt: '2024-01-05T10:12:56.029922Z', label: nomobile, value: nomobile}, {id: 17873, form_id: 11240, CreatedAt: '2024-01-05T10:12:56.030959Z', UpdatedAt: '2024-01-05T10:12:56.030959Z', label: stopsms, value: stopsms}, {id: 17874, form_id: 11240, CreatedAt: '2024-01-05T10:12:56.031673Z', UpdatedAt: '2024-01-05T10:12:56.031673Z', label: consent_email, value: consent_email}, {id: 17875, form_id: 11240, CreatedAt: '2024-01-05T10:12:56.032365Z', UpdatedAt: '2024-01-05T10:12:56.032365Z', label: consent_mobile, value: consent_mobile}, {id: 17876, form_id: 11240, CreatedAt: '2024-01-05T10:12:56.033305Z', UpdatedAt: '2024-01-05T10:12:56.033305Z', label: no_consent_email, value: no_consent_email}, {id: 17877, form_id: 11240, CreatedAt: '2024-01-05T10:12:56.033911Z', UpdatedAt: '2024-01-05T10:12:56.033911Z', label: no_consent_mobile, value: no_consent_mobile}, {id: 17878, form_id: 11240, CreatedAt: '2024-01-05T10:12:56.034457Z', UpdatedAt: '2024-01-05T10:12:56.034457Z', label: double_optin_sms, value: double_optin_sms}, {id: 17879, form_id: 11240, CreatedAt: '2024-01-05T10:12:56.035242Z', UpdatedAt: '2024-01-05T10:12:56.035242Z', label: yes, value: yes}]}]}, status: success}}, Survey: {value: {data: {forms: [{id: 12515, group_id: [631], CreatedAt: '2025-01-16T17:19:59.120908Z', UpdatedAt: '2025-01-16T17:19:59.120908Z', label: 'On a scale of 1 to 10, how would you rate your satisfaction with the council?', order: 1, section: 'My city and me', type: range, refvalues: [{CreatedAt: '2025-01-16T17:19:59.128542Z', UpdatedAt: '2025-01-16T17:19:59.128542Z', form_id: 12515, id: 19477, max: 10, step: 1}]}, {id: 12516, group_id: [631], CreatedAt: '2025-01-16T17:19:59.132315Z', UpdatedAt: '2025-01-16T17:19:59.132315Z', label: 'What are the strengths of the town/city in your opinion?', section: 'My city and me', type: checkbox, order: 2, refvalues: [{CreatedAt: '2025-01-16T17:19:59.132789Z', UpdatedAt: '2025-01-16T17:19:59.132789Z', form_id: 12516, id: 19478, label: 'Public infrastructure', value: public_infrastructure}, {CreatedAt: '2025-01-16T17:19:59.132789Z', UpdatedAt: '2025-01-16T17:19:59.132789Z', form_id: 12516, id: 19479, label: Education, value: education}, {CreatedAt: '2025-01-16T17:19:59.132789Z', UpdatedAt: '2025-01-16T17:19:59.132789Z', form_id: 12516, id: 19480, label: 'Parks and green areas', value: parks_and_green_areas}, {CreatedAt: '2025-01-16T17:19:59.132789Z', UpdatedAt: '2025-01-16T17:19:59.132789Z', form_id: 12516, id: 19481, label: Roads, value: roads}, {CreatedAt: '2025-01-16T17:19:59.132789Z', UpdatedAt: '2025-01-16T17:19:59.132789Z', form_id: 12516, id: 19482, label: 'Neighborhood living', value: neighborhood_living}, {CreatedAt: '2025-01-16T17:19:59.132789Z', UpdatedAt: '2025-01-16T17:19:59.132789Z', form_id: 12516, id: 19483, label: 'Sports infrastructures', value: sports_infrastructures}]}, {id: 12517, group_id: [631], CreatedAt: '2025-01-16T17:19:59.134634Z', UpdatedAt: '2025-01-16T17:19:59.134634Z', label: 'What do you think should be improved?', order: 3, section: 'My city and me', type: checkbox, refvalues: [{CreatedAt: '2025-01-16T17:19:59.135064Z', UpdatedAt: '2025-01-16T17:19:59.135064Z', form_id: 12517, id: 19484, label: 'Public infrastructure', value: public_infrastructure}, {CreatedAt: '2025-01-16T17:19:59.135064Z', UpdatedAt: '2025-01-16T17:19:59.135064Z', form_id: 12517, id: 19485, label: Education, value: education}, {CreatedAt: '2025-01-16T17:19:59.135064Z', UpdatedAt: '2025-01-16T17:19:59.135064Z', form_id: 12517, id: 19486, label: 'Parks and green areas', value: parks_and_green_areas}, {CreatedAt: '2025-01-16T17:19:59.135064Z', UpdatedAt: '2025-01-16T17:19:59.135064Z', form_id: 12517, id: 19487, label: Roads, value: roads}, {CreatedAt: '2025-01-16T17:19:59.135064Z', UpdatedAt: '2025-01-16T17:19:59.135064Z', form_id: 12517, id: 19488, label: 'Neighborhood living', value: neighborhood_living}, {CreatedAt: '2025-01-16T17:19:59.135064Z', UpdatedAt: '2025-01-16T17:19:59.135064Z', form_id: 12517, id: 19489, label: 'Sports infrastructures', value: sports_infrastructures}]}, {id: 12518, group_id: [631], CreatedAt: '2025-01-16T17:19:59.136414Z', UpdatedAt: '2025-01-16T17:19:59.136414Z', label: 'Do you have anything to bring up?', order: 4, section: 'My city and me', type: text, refvalues: [{CreatedAt: '2025-01-16T17:19:59.136896Z', UpdatedAt: '2025-01-16T17:19:59.136896Z', form_id: 12518, id: 19490}]}]}, status: success}}, 'Level of support': {value: {data: {forms: [{id: 11238, group_id: [631], CreatedAt: '2024-01-05T10:12:56.016111Z', UpdatedAt: '2024-01-05T10:12:56.016111Z', label: 'Level of support', type: radio, refvalues: [{id: 17863, form_id: 11238, CreatedAt: '2024-01-05T10:12:56.016769Z', UpdatedAt: '2024-01-05T10:12:56.016769Z', color: '#05B66F', followed: true, label: 'Elected official', value: elected_official}, {id: 17864, form_id: 11238, CreatedAt: '2024-01-05T10:12:56.017383Z', UpdatedAt: '2024-01-05T10:12:56.017383Z', color: '#05B66F', followed: true, label: Association, value: association}, {id: 17865, form_id: 11238, CreatedAt: '2024-01-05T10:12:56.017919Z', UpdatedAt: '2024-01-05T10:12:56.017919Z', color: '#7AD8F7', followed: true, label: Business, value: business}, {id: 17866, form_id: 11238, CreatedAt: '2024-01-05T10:12:56.018642Z', UpdatedAt: '2024-01-05T10:12:56.018642Z', color: '#FF8383', followed: true, label: Citizen, value: citizen}, {id: 17867, form_id: 11238, CreatedAt: '2024-01-05T10:12:56.019282Z', UpdatedAt: '2024-01-05T10:12:56.019282Z', color: '#FF8383', followed: true, label: 'To be convinced', value: to_be_convinced}]}]}, status: success}}, 'Presence status': {value: {data: {forms: [{id: 11239, group_id: [631], CreatedAt: '2024-01-05T10:12:56.022876Z', UpdatedAt: '2024-01-05T10:12:56.022876Z', type: radio, label: 'Presence status', refvalues: [{id: 17868, form_id: 11239, CreatedAt: '2024-01-05T10:12:56.023542Z', UpdatedAt: '2024-01-05T10:12:56.023542Z', label: present, value: present}, {id: 17869, form_id: 11239, CreatedAt: '2024-01-05T10:12:56.024299Z', UpdatedAt: '2024-01-05T10:12:56.024299Z', label: absent, value: absent}, {id: 17870, form_id: 11239, CreatedAt: '2024-01-05T10:12:56.024868Z', UpdatedAt: '2024-01-05T10:12:56.024868Z', label: refusal, value: refus}, {id: 17871, form_id: 11239, CreatedAt: '2024-01-05T10:12:56.025662Z', UpdatedAt: '2024-01-05T10:12:56.025662Z', label: repasse, value: repasse}]}]}, status: success}}, Tasks: {value: {data: {forms: [{id: 11236, group_id: [631], CreatedAt: '2024-01-05T10:12:56.004827Z', UpdatedAt: '2024-01-05T10:12:56.004827Z', label: 'Next steps', order: 1, type: checkbox, refvalues: [{id: 17858, form_id: 11236, followed: true, CreatedAt: '2024-01-05T10:12:56.005509Z', UpdatedAt: '2024-01-05T10:12:56.005509Z', label: 'Receive our information', value: receive_our_information}, {id: 17859, form_id: 11236, followed: true, CreatedAt: '2024-01-05T10:12:56.006214Z', UpdatedAt: '2024-01-05T10:12:56.006214Z', label: 'Provide follow-up', type: other, value: provide_follow_up}, {id: 17860, form_id: 11236, followed: true, CreatedAt: '2024-01-05T10:12:56.006795Z', UpdatedAt: '2024-01-05T10:12:56.006795Z', label: 'Report a problem', type: report, value: report_a_problem}, {id: 17861, form_id: 11236, followed: true, CreatedAt: '2024-01-05T10:12:56.007569Z', UpdatedAt: '2024-01-05T10:12:56.007569Z', label: 'Invite to an event', type: invite, value: invite_to_event}]}, {id: 11237, group_id: [631], CreatedAt: '2024-01-05T10:12:56.010989Z', UpdatedAt: '2024-01-05T10:12:56.010989Z', label: Notes, order: 2, type: text, refvalues: [{id: 17862, form_id: 11237, CreatedAt: '2024-01-05T10:12:56.011856Z', UpdatedAt: '2024-01-05T10:12:56.011856Z'}]}]}, status: success}}, 'Custom fields': {value: {data: {forms: [{id: 12519, group_id: [631], CreatedAt: '2025-01-17T17:22:52.730998Z', UpdatedAt: '2025-01-17T17:22:52.730998Z', section: identity, type: text, hidden: true, label: Job, mandatory: true, order: 10, refvalues: [{id: 19491, form_id: 12519, CreatedAt: '2025-01-17T17:22:52.737235Z', UpdatedAt: '2025-01-17T17:22:52.737235Z'}]}, {id: 12520, group_id: [631], CreatedAt: '2025-01-17T17:22:52.744444Z', UpdatedAt: '2025-01-17T17:22:52.744444Z', section: identity, type: date, hidden: true, label: 'Date of first job', order: 11, refvalues: [{id: 19492, form_id: 12520, CreatedAt: '2025-01-17T17:22:52.745014Z', UpdatedAt: '2025-01-17T17:22:52.745014Z'}]}, {id: 12521, group_id: [631], CreatedAt: '2025-01-17T17:22:52.746548Z', UpdatedAt: '2025-01-17T17:22:52.746548Z', section: identity, type: numeric, hidden: true, label: 'Number of children', order: 12, refvalues: [{form_id: 12521, id: 19493, CreatedAt: '2025-01-17T17:22:52.747327Z', UpdatedAt: '2025-01-17T17:22:52.747327Z'}]}, {id: 12522, group_id: [631], CreatedAt: '2025-01-17T17:22:52.748828Z', UpdatedAt: '2025-01-17T17:22:52.748828Z', section: infos, type: text, hidden: true, label: 'Member ID', order: 16, refvalues: [{form_id: 12522, id: 19494, CreatedAt: '2025-01-17T17:22:52.749363Z', UpdatedAt: '2025-01-17T17:22:52.749363Z'}]}, {id: 12523, group_id: [631], CreatedAt: '2025-01-17T17:22:52.750458Z', UpdatedAt: '2025-01-17T17:22:52.750458Z', section: address, type: text, hidden: true, label: 'Door color', order: 29, refvalues: [{form_id: 12523, id: 19495, CreatedAt: '2025-01-17T17:22:52.750905Z', UpdatedAt: '2025-01-17T17:22:52.750905Z'}]}]}, status: success}}}}}}}
components:
    securitySchemes:
        bearerAuth:
            type: http
            scheme: bearer
    examples:
        minimalSearch:
            value: {data: {advanced_search: {per_page: 1000, query: {$all: []}}}}
        advancedSearchSimple:
            value: {data: {advanced_search: {page: 0, per_page: 50, sort_asc: true, sort_attr: surname, query: {$all: [{$at_least_one: [{$condition: {attr: address.city, ope: eql, value: Bordeaux}}, {$condition: {attr: address.city, ope: eql, value: Talence}}, {$condition: {attr: address.city, ope: eql, value: Mérignac}}]}]}}}}
            description: "**Search contacts with:**\n(address.city is equal to Bordeaux\nOR address.city is equal to Talence\nOR address.city is equal to Mérignac)"
        advancedSearchFull:
            value: {data: {advanced_search: {page: 0, per_page: 50, sort_asc: true, sort_attr: surname, include_interactions: true, query: {$all: [{$at_least_one: [{$condition: {attr: mobile, ope: ext}}, {$condition: {attr: phone, ope: ext}}]}, {$all: [{$condition: {attr: form, form_id: 4530, form_ref_ids: [7486], ope: eql, value: null}}]}, {$all: [{$condition: {attr: form, form_id: 544, form_ref_ids: [6420], ope: eql, value: null}}]}]}}}}
            description: "**Search contacts with:**\n(mobile exist OR phone exist)\nAND (ref value of form 4530 is equal to 7486)\n*→ (ex: form 4530 is \"Consentement RGPD\" and ref value 7486 is \"OK for call and SMS\")*\nAND (ref value of form 544 is equal to 6420)\n*→ (ex: form 544 is \"Level of support\" and ref value 6420 is \"indecisive\")*"
        searchByTransaction:
            value: {data: {advanced_search: {per_page: 100, query: {$all: [{$all: [{$condition: {attr: transactions.sum_amount, ope: gte, value: 100}}]}]}}}}
            description: "**Find contacts whose total transaction amount is >= €100.**\n`transactions.sum_amount` is the cumulative amount across all transactions for the contact."
        searchByDonation:
            value: {data: {advanced_search: {per_page: 100, query: {$all: [{$all: [{$condition: {attr: donations.count_amount, ope: gte, value: 1}}]}, {$all: [{$condition: {attr: donations.created_at, ope: range, from: '2024-01-01T00:00:00Z', to: '2024-12-31T23:59:59Z'}}]}]}}}}
            description: '**Find contacts who made at least one donation in 2024.**'
        searchByMembership:
            value: {data: {advanced_search: {per_page: 100, query: {$all: [{$all: [{$condition: {attr: memberships.end_date, ope: gte, value: now}}]}, {$all: [{$condition: {attr: memberships.start_date, ope: lte, value: now}}]}]}}}}
            description: "**Find contacts with a currently active membership** (start_date <= now AND end_date >= now).\nRelative dates such as `\"now\"`, `\"now-30d\"`, `\"now-1y\"` are supported."
        searchByEmailCampaign:
            value: {data: {advanced_search: {per_page: 100, query: {$all: [{$all: [{$condition: {attr: emails.campaign_id, ope: eql, value: '42'}}]}, {$all: [{$condition: {attr: emails.opened_at, ope: ext}}]}]}}}}
            description: "**Find contacts who received campaign ID 42 and opened the email.**\nUse `emails.clicked_at`, `emails.bounced_at`, `emails.unsubscribed_at`\nsimilarly to filter by other email engagement events."
    schemas:
        Contact:
            type: object
            properties: {firstname: {type: string, description: 'First name of the contact.', example: Jeanne}, surname: {type: string, example: Dupont, description: 'Last name / surname of the contact.'}, married_name: {type: string, example: Dupont, description: 'Married name of the contact.'}, gender: {type: string, example: F, description: 'Gender of the contact. Possible values: `F`, `M`, `O`.'}, birthdate: {type: string, format: date-time, example: '1955-07-06T00:00:00Z', description: 'Date of birth.'}, age_category: {type: integer, description: 'Age category of the contact. Range 0–6 by default (configurable). 0 = 18 or under, 1 = 18–25, etc.'}, birthdept: {type: string, description: 'Birth county/department of the contact.'}, birthcity: {type: string, description: 'Birth city of the contact.'}, birthcountry: {type: string, description: 'Birth country of the contact.'}, nationality: {type: string, description: 'Nationality of the contact.'}, mail: {type: string, format: email, description: 'Email address of the contact.'}, phone: {type: string, description: 'Landline phone number of the contact.'}, mobile: {type: string, description: 'Mobile phone number of the contact.'}, address: {$ref: '#/components/schemas/Address'}, tags: {type: array, items: {type: object, required: [name], properties: {name: {type: string}}}}, actions_ids: {type: array, items: {type: integer}, description: 'IDs of actions linked to this contact.'}, user_id: {type: integer, description: 'ID of the last user who updated the contact.'}, user_contact_id: {type: integer, description: 'ID of the Qomon user linked to this contact.'}, formdatas: {type: array, items: {$ref: '#/components/schemas/FormData'}, description: 'Survey, level of support, presence status, and consent answers for the contact.'}, customfields: {type: array, items: {$ref: '#/components/schemas/FormData'}, description: 'Custom field values for the contact.'}, black_list: {type: boolean, description: 'Prevent any communication with this contact.', example: true}, notes: {type: array, items: {type: object, required: [content], properties: {id: {type: integer}, content: {type: string}, pinned: {type: boolean}}}}}
        ContactResponse:
            allOf: [{type: object, properties: {id: {type: integer, example: 12345678}, group_id: {type: integer, example: 1}, CreatedAt: {type: string, format: date-time, example: '2022-10-13T15:50:34.643724Z'}, UpdatedAt: {type: string, format: date-time, example: '2022-10-13T15:55:12.853724Z'}, lastchange: {type: string, example: '2022-10-13T15:50:24.163Z'}}}, {$ref: '#/components/schemas/Contact'}, {type: object, properties: {address: {$ref: '#/components/schemas/AddressResp'}, tags: {type: array, items: {$ref: '#/components/schemas/Tag'}}, formdatas: {type: array, items: {$ref: '#/components/schemas/FormDataResp'}}, customfields: {type: array, items: {$ref: '#/components/schemas/FormDataResp'}}}}]
        ContactResponseWithInteractions:
            allOf: [{$ref: '#/components/schemas/ContactResponse'}, {type: object, properties: {interactions: {type: array, items: {type: object, properties: {id: {type: integer}, created_at: {type: string, format: date-time}, updated_at: {type: string, format: date-time}, type: {type: string, enum: [letter, call]}, sub_type: {type: string, enum: [incoming, outgoing]}, title: {type: string}, comments: {type: string}, contact_id: {type: integer}, group_id: {type: integer}, user_id: {type: integer}, user_contact_id: {type: integer}, address: {type: string}, only_super_admin: {type: boolean}, files: {type: array, items: {type: object, properties: {id: {type: integer}, created: {type: string, format: date-time}, owner_id: {type: integer}, group_id: {type: integer}, subject: {type: string}, type: {type: string}, url: {type: string}, folder: {type: string}, mime: {type: string}, name: {type: string}, category: {type: string}, category_id: {type: integer}, cover: {type: string}, minimum_role: {type: string}, is_a_team_file: {type: boolean}, is_interaction: {type: boolean}, private: {type: boolean}}}}}}}, notes: {type: array, items: {type: object, properties: {id: {type: integer}, created_at: {type: string, format: date-time}, updated_at: {type: string, format: date-time}, data: {type: string}, contact_id: {type: integer}, group_id: {type: integer}, user_id: {type: integer}, pinned: {type: boolean}, only_super_admin: {type: boolean}}}}}}]
        ContactUpsert:
            type: object
            additionalProperties: false
            properties: {id: {type: integer}, firstname: {type: string, example: John}, surname: {type: string, example: Doe}, married_name: {type: string, example: Smith}, gender: {type: string, enum: [M, F, O]}, birthdate: {type: string, format: date-time}, age_category: {type: integer}, birthdept: {type: string}, birthcity: {type: string}, birthcountry: {type: string}, mail: {type: string, format: email}, phone: {type: string}, mobile: {type: string}, address: {$ref: '#/components/schemas/Address'}, tags: {type: array, items: {type: object, required: [name], properties: {name: {type: string}}}}, nationbuilderid: {type: integer}, black_list: {type: boolean}, name_presences: {$ref: '#/components/schemas/FormsLight'}, consents: {$ref: '#/components/schemas/FormsLight'}, actions: {$ref: '#/components/schemas/FormsLight'}, forms: {$ref: '#/components/schemas/FormsLight'}, custom_fields: {$ref: '#/components/schemas/FormsLight'}, status: {$ref: '#/components/schemas/FormsLight'}}
        Tag:
            type: object
            properties: {name: {type: string}, appearance_count: {type: integer}, color: {type: string}}
        Address:
            type: object
            properties: {housenumber: {type: string, example: 1}, street: {type: string, example: 'rue de la paix'}, postalcode: {type: string, example: 75000}, citycode: {type: string, example: 75101}, city: {type: string, example: Paris}, state: {type: string, example: Ile-de-France}, country: {type: string, example: France}, addition: {type: string, example: 'Résidence de la paix'}, infos: {type: string, example: 'Porte de gauche'}, building: {type: string, example: 'Bâtiment A'}, floor: {type: string, example: 2}, door: {type: string, example: A}, pollingstation: {type: string, example: BV-06}}
        AddressResp:
            allOf: [{type: object, properties: {id: {type: integer, example: 1001}}}, {$ref: '#/components/schemas/Address'}, {type: object, properties: {score: {type: number, example: 0.965753}, latitude: {type: string, example: '48.000000'}, longitude: {type: string, example: '2.000000'}, location: {type: string, example: '48.000000,2.000000'}, invalid: {type: boolean, nullable: true, example: null}}}]
        Form:
            type: object
            description: "Represents a form field with its type (`numeric`, `radio`, ...) and\nits possible values and/or constraints on it (`refvalues`).\nA form is used to describe a contact via formdata."
            properties: {id: {type: integer}, CreatedAt: {type: string}, UpdatedAt: {type: string}, group_id: {type: array, items: {type: integer}}, type: {type: string, enum: [radio, checkbox, date, report, meet, text, signature, photos, numeric, range, other]}, section: {type: string}, activate: {type: string}, order: {type: integer}, label: {type: string}, refvalues: {type: array, items: {$ref: '#/components/schemas/RefValue'}}, mandatory: {type: boolean}}
        RefValue:
            type: object
            properties: {id: {type: integer}, CreatedAt: {type: string}, UpdatedAt: {type: string}, form_id: {type: integer}, label: {type: string}, value: {type: string}, min: {type: integer}, max: {type: integer}, step: {type: integer}, type: {type: string}, followed: {type: boolean}, color: {type: string}}
        FormsLight:
            type: array
            items: {type: object, anyOf: [{required: [id, value], properties: {id: {type: integer, description: 'ID of the form'}, value: {type: string}}}, {required: [label, value], properties: {label: {type: string, description: 'Label of the form'}, value: {type: string}}}, {required: [id, label, value], properties: {id: {type: integer, description: 'ID of the form'}, label: {type: string, description: 'Label of the form'}, value: {type: string}}}]}
        FormData:
            type: object
            required: [form_id, form_ref_id]
            properties: {form_id: {type: integer}, form_ref_id: {type: integer}, data: {type: string}}
        FormDataResp:
            description: "A formdata is a response to a form. It links a contact, a form and a refvalue.\nA contact (`contact_id`) answers a form (`form_id`) by selecting\na refvalue (`form_ref_id`) with a value (`data`)."
            type: object
            properties: {id: {type: integer, example: 3001}, created_at: {type: string, format: date-time, example: '2022-10-13T15:50:24.163Z'}, updated_at: {type: string, format: date-time, example: '2022-10-13T15:50:24.163Z'}, deleted_at: {type: string, format: date-time, example: '2022-10-13T15:50:24.163Z', nullable: true}, data: {type: string, example: Oui}, date: {type: string, format: date-time, example: '2022-10-13T15:50:24.163Z'}, donedate: {type: string, format: date-time, nullable: true, example: null}, label: {type: string, nullable: true, example: null}, type_of_form: {type: string, nullable: true, example: null}, group_id: {type: integer, example: 1}, contact_id: {type: integer, example: 12345678, description: 'ID of the contact'}, form_id: {type: integer, example: 1001, description: 'ID of the form'}, form_ref_id: {type: integer, example: 2001, description: 'ID of the refvalue of the form'}}
        AdvancedSearchCondition:
            type: object
            additionalProperties: false
            properties: {$condition: {type: object, oneOf: [{required: [attr, ope], additionalProperties: false, properties: {attr: {type: string, example: surname, description: Attribute, enum: [firstname, surname, married_name, gender, birthdate, age_cat, birthcity, birthcountry, mail, mobile, phone, address.building, address.floor, address.door, address.housenumber, address.street, address.addition, address.postalcode, address.city, address.country, address.pollingstation, address.invalid, tags.name, lastchange, user_id, user_contact_id, action_ids, black_list, petitions.base_id, onlineforms.base_id, transactions.created_at, transactions.date, transactions.amount, transactions.code_campaign, transactions.payment_method_kind, transactions.status_id, transactions.sum_amount, transactions.count_amount, donations.created_at, donations.transaction_date, donations.amount, donations.donation_price_id, donations.sum_amount, donations.count_amount, memberships.created_at, memberships.start_date, memberships.end_date, memberships.transaction_date, memberships.period, memberships.multiple_in_period, memberships.membership_price_id, emails.created_at, emails.type, emails.sender, emails.subject, emails.user_id, emails.campaign_id, emails.campaign_name, emails.campaign_goal, emails.campaign_is_successful, emails.campaign_performance_rating, emails.template_id, emails.template_name, emails.delivered_at, emails.opened_at, emails.clicked_at, emails.bounced_at, emails.unsubscribed_at, emails.blacklisted_at, emails.abuse_report_at, emails.clicked_urls]}, ope: {type: string, example: eq, description: "Operator.\n- `ext`, `not_ext`: all fields\n- `eql`, `not_eql`: most fields (see attribute table above)\n- `start_with`, `not_start_with`: firstname, surname, married_name, mail, mobile, phone, address.street, address.postalcode, address.city, address.invalid\n- `lte`, `gte`, `range`: birthdate, lastchange, amount fields, date fields on nested objects\n- `eql:strictdata`, `not_eql:strictdata`: mail\n- `contains`, `not_contains`: text fields, transactions.code_campaign, emails.campaign_name\n- `period`: date fields (relative period matching)", enum: [eql, not_eql, ext, not_ext, range, lte, gte, contains, not_contains, start_with, not_start_with, 'eql:strictdata', 'not_eql:strictdata']}, value: {description: 'Value to match. Use a string for text/date fields, a number for amount or count fields.', nullable: true, example: Dupont, oneOf: [{type: string}, {type: number}]}, from: {description: 'Start of range — ISO 8601 date-time for date fields, number for amount fields. Required when `ope` is `range`.', nullable: true, example: '1955-07-06T00:00:00Z', oneOf: [{type: string}, {type: number}]}, to: {description: 'End of range — ISO 8601 date-time for date fields, number for amount fields. Required when `ope` is `range`.', nullable: true, example: '1955-07-06T00:00:00Z', oneOf: [{type: string}, {type: number}]}}}, {required: [attr, ope, form_id, form_ref_ids], additionalProperties: false, properties: {attr: {type: string, example: form, description: Attribute, enum: [custom_fields, form]}, ope: {type: string, example: eq, description: "Operator.\n- `ext`, `not_ext`: all fields\n- `eql`, `not_eql`: forms with type 'text', 'radio', 'checkbox', 'range' or 'date'\n- `contains`, `not_contains`, `start_with`, `not_start_with`: forms with type 'text'\n- `lte`, `gte`, `range`: forms with type 'date'", enum: [eql, not_eql, ext, not_ext, range, lte, gte, start_with, not_start_with, contains, not_contains]}, form_id: {type: integer, example: 4250, description: 'If attr is form, the form_id is required'}, form_ref_ids: {type: array, description: 'If attr is form, the form_ref_ids is required', items: {type: integer}, example: [6648]}, value: {type: string, description: 'Value of the form_ref_ids if necessary (depends of the fields and the operator)', nullable: true, example: Yes}, from: {type: string, format: date-time, description: 'Required only if "ope" is `range`', nullable: true, example: '1955-07-06T00:00:00Z'}, to: {type: string, format: date-time, description: 'Required only if "ope" is `range`', nullable: true, example: '1955-07-06T00:00:00Z'}}}]}}
        AdvancedSearchNode:
            oneOf: [{required: [$all], properties: {$all: {type: array, items: {$ref: '#/components/schemas/AdvancedSearchCondition'}}}}, {required: [$at_least_one], properties: {$at_least_one: {type: array, items: {$ref: '#/components/schemas/AdvancedSearchCondition'}}}}]
        Search:
            type: object
            required: [advanced_search]
            properties: {advanced_search: {type: object, required: [query, per_page], additionalProperties: false, properties: {page: {type: integer, minimum: 0, example: 0}, per_page: {type: integer, maximum: 1000, minimum: 1, example: 50}, sort_asc: {type: boolean, example: true}, sort_attr: {type: string, enum: [surname, firstname, birthdate, gender, lastchange, mail, married_name, city], example: surname}, query: {type: object, oneOf: [{required: [$all], properties: {$all: {type: array, items: {$ref: '#/components/schemas/AdvancedSearchNode'}}}}, {required: [$at_least_one], properties: {$at_least_one: {type: array, items: {$ref: '#/components/schemas/AdvancedSearchNode'}}}}]}}}, include_interactions: {type: boolean, example: true}}
        KpiSearch:
            type: object
            required: [advanced_search]
            properties: {advanced_search: {type: object, required: [query], additionalProperties: false, properties: {query: {type: object, oneOf: [{required: [$all], properties: {$all: {type: array, items: {$ref: '#/components/schemas/AdvancedSearchNode'}}}}, {required: [$at_least_one], properties: {$at_least_one: {type: array, items: {$ref: '#/components/schemas/AdvancedSearchNode'}}}}]}}}}
