{
  "openapi": "3.1.0",
  "info": {
    "title": "NexaCloser API",
    "version": "1.0.0",
    "summary": "Create and update CRM contacts, register webinar attendees, read website analytics, receive webhooks and connect AI clients over MCP.",
    "description": "The NexaCloser REST API. All endpoints are served over HTTPS and speak JSON.\n\n**Base URL:** `https://api.nexacloser.com/v1`\n\n**Authentication:** an API key from **Settings → API** in the NexaCloser app, sent as\n`Authorization: Bearer nxa_…`. Keys are secrets: call the API from your server, never from\nbrowser code. See [Authentication](https://docs.nexacloser.com/authentication/) for key types and access levels.\n\n**Errors** are JSON objects with an `error` code. **Rate limits** are reported in\n`X-RateLimit-*` headers; a `429` carries `Retry-After`.\n",
    "contact": {
      "name": "NexaCloser support",
      "email": "support@nexacloser.com",
      "url": "https://docs.nexacloser.com"
    }
  },
  "servers": [
    {
      "url": "https://api.nexacloser.com/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Contacts",
      "description": "Create or update CRM contacts from your website, backend or another system."
    },
    {
      "name": "Webinars",
      "description": "Register attendees for a NexaCloser webinar from your own landing page."
    },
    {
      "name": "Website analytics",
      "description": "Read visitor numbers for the websites tracked by NexaCloser analytics."
    },
    {
      "name": "Inbound webhooks",
      "description": "Send any payload to a webhook URL created in NexaCloser to start automations."
    },
    {
      "name": "MCP",
      "description": "The Model Context Protocol endpoint for AI clients (Claude, Cursor and others)."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/contacts": {
      "post": {
        "tags": [
          "Contacts"
        ],
        "operationId": "upsertContact",
        "summary": "Create or update a contact",
        "description": "Creates a CRM contact, or updates the matching one.\n\n**Matching.** Inside your workspace, among live (not deleted) contacts, NexaCloser looks for\nthe same phone number first, then the same email. A match is **updated**: non-empty values\nyou send overwrite, empty values keep what is there, `tags` are added to the existing tags and\n`custom_fields` are merged into the existing values. No match: a new contact is **created**.\nIf the workspace turned on *allow duplicate contacts*, every call creates a new contact.\n\n**Source.** Every contact created through the API gets the CRM source `api`. The `source`\nlabel you send (for example `Website contact form`) is saved as the contact's source\nchannel, only when the contact is created; an existing contact keeps its own.\n\n**Automations.** Creating a contact fires the workspace's *contact created* automations. New\ntags fire *tag added* automations.\n\n**Consent.** `consent.granted: true` records an opt-in per channel. `granted: false` records\nnothing: an unticked box is not an opt-out. A person who opted out earlier stays opted out.\n\n**What comes back.** Only the contact id, whether it was created, how it was matched, the\nconsent outcome per channel and warnings. Other data of an existing contact is never\nreturned, so a contacts-only key can write but cannot read your CRM.\n\n**Warnings.** Fields the endpoint does not know are ignored and listed in `warnings`\n(`unknown_field:<name>`, `unknown_attribution_key:<key>`), as are custom field keys with no\nfield defined in the workspace (`unknown_custom_field:<key>`, still saved).\n\n**Test keys** (`nxa_test_…`) validate the request exactly like a live key but write nothing\nand read no contact: the response is `200` with `test: true` and the normalized values.\n\n**Access.** Works with a full-access key or a *Contacts: create & update only* key. Call it\nfrom your server; the only browser origin accepted is the try-it console on\ndocs.nexacloser.com.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactUpsert"
              },
              "examples": {
                "websiteForm": {
                  "summary": "Website form with marketing consent",
                  "value": {
                    "email": "dana@example.com",
                    "phone": "+14155550101",
                    "first_name": "Dana",
                    "last_name": "Levi",
                    "source": "Website contact form",
                    "tags": [
                      "website",
                      "consult-request"
                    ],
                    "custom_fields": {
                      "visit_type": "first_visit",
                      "contact_time": "morning"
                    },
                    "attribution": {
                      "utm_source": "google",
                      "utm_medium": "cpc",
                      "utm_campaign": "fall-promo",
                      "landing_url": "https://www.example.com/contact"
                    },
                    "consent": {
                      "granted": true,
                      "channels": [
                        "email",
                        "sms"
                      ],
                      "text": "I agree to receive emails and text messages from Example Clinic. Reply STOP to opt out.",
                      "version": "2026-11-01",
                      "at": "2026-11-05T14:03:11Z",
                      "ip": "203.0.113.24",
                      "page_url": "https://www.example.com/contact"
                    }
                  }
                },
                "minimal": {
                  "summary": "Email only",
                  "value": {
                    "email": "dana@example.com",
                    "full_name": "Dana Levi"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An existing contact was updated, or (test key) the request is valid and nothing was written.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ContactUpsertResult"
                    },
                    {
                      "$ref": "#/components/schemas/ContactTestResult"
                    }
                  ]
                },
                "examples": {
                  "updated": {
                    "summary": "Matched by email and updated",
                    "value": {
                      "id": "cnt_8hT2kQx91L",
                      "created": false,
                      "matched_by": "email",
                      "consent": [
                        {
                          "channel": "email",
                          "status": "granted"
                        },
                        {
                          "channel": "sms",
                          "status": "withdrawn",
                          "skipped": true
                        }
                      ],
                      "warnings": [
                        "unknown_custom_field:contact_time"
                      ]
                    }
                  },
                  "testKey": {
                    "summary": "Test key, nothing written",
                    "value": {
                      "test": true,
                      "valid": true,
                      "would": "create_or_update",
                      "normalized": {
                        "email": "dana@example.com",
                        "phone": "14155550101",
                        "full_name": "Dana Levi",
                        "source": "Website contact form",
                        "tags": [
                          "website",
                          "consult-request"
                        ],
                        "custom_fields": {
                          "visit_type": "first_visit",
                          "contact_time": "morning"
                        },
                        "preferred_language": null,
                        "attribution": {
                          "utm_source": "google",
                          "utm_medium": "cpc",
                          "landing_url": "https://www.example.com/contact"
                        },
                        "consent": {
                          "granted": true,
                          "channels": [
                            "email",
                            "sms"
                          ],
                          "purpose": "marketing",
                          "text": "I agree to receive emails and text messages from Example Clinic. Reply STOP to opt out.",
                          "version": "2026-11-01",
                          "at": "2026-11-05T14:03:11.000Z",
                          "language": "en",
                          "ip": "203.0.113.24",
                          "user_agent": null,
                          "page_url": "https://www.example.com/contact"
                        }
                      },
                      "warnings": [
                        "unknown_custom_field:visit_type",
                        "unknown_custom_field:contact_time"
                      ]
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "A new contact was created.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactUpsertResult"
                },
                "example": {
                  "id": "cnt_8hT2kQx91L",
                  "created": true,
                  "matched_by": null,
                  "consent": [
                    {
                      "channel": "email",
                      "status": "granted"
                    },
                    {
                      "channel": "sms",
                      "status": "granted"
                    }
                  ],
                  "warnings": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidJson"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key is valid but its access level does not include this endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "413": {
            "description": "The body is larger than 64 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "payload_too_large"
                }
              }
            }
          },
          "422": {
            "description": "The body is valid JSON but a field is wrong. Every problem is listed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                },
                "example": {
                  "error": "validation_failed",
                  "details": [
                    {
                      "field": "email",
                      "code": "invalid_email"
                    },
                    {
                      "field": "custom_fields.visit_type",
                      "code": "invalid_custom_field_value"
                    },
                    {
                      "field": "consent.text",
                      "code": "consent_text_required"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/webinar-registrants": {
      "post": {
        "tags": [
          "Webinars"
        ],
        "operationId": "registerWebinarAttendees",
        "summary": "Register attendees for a webinar",
        "description": "Registers one attendee or a batch of up to 100 for one of your webinars, exactly as the\nhosted registration form would: the contact is created or updated in the CRM, the\nattendee gets a session and a personal watch link, and the webinar's automations run.\n\nPick the webinar with `webinar_id` or `slug` (`webinar_id` wins when both are sent). It\nmust belong to the key's workspace and be active.\n\n**Consent.** A registrant with a phone number must carry `consent.granted: true`, or that\nregistrant is refused with `consent_required` (the rest of the batch still goes through).\nEmail-only registrants need no consent object.\n\n**Repeats.** Registering the same person for the same session again is a success that\nreturns the existing registration and link, with `duplicate: true`.\n\nEach item in `results` succeeds or fails on its own; the call itself returns `200` once\nthe batch is processed.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegistrantsRequest"
              },
              "example": {
                "webinar_id": "wbn_6RLC77yktP",
                "registrants": [
                  {
                    "first_name": "Dana",
                    "last_name": "Levi",
                    "email": "dana@example.com",
                    "phone": "+972501234567",
                    "utm_source": "facebook",
                    "page_url": "https://www.example.com/webinar?utm_source=facebook",
                    "consent": {
                      "granted": true,
                      "text_shown": "I agree to get webinar reminders by WhatsApp and SMS.",
                      "source": "landing_page"
                    }
                  },
                  {
                    "email": "avi@example.com",
                    "first_name": "Avi"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The batch was processed. Check `results` for each registrant.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistrantsResponse"
                },
                "example": {
                  "results": [
                    {
                      "index": 0,
                      "ok": true,
                      "registration_id": "wbr_5D2AkHpKwL",
                      "personal_link_url": "https://app.nexacloser.com/j/wbr_5D2AkHpKwL.6b146474.59934ce894cb",
                      "duplicate": false
                    },
                    {
                      "index": 1,
                      "ok": false,
                      "error": "consent_required"
                    }
                  ],
                  "created": 1,
                  "duplicates": 0,
                  "failed": 1
                }
              }
            }
          },
          "400": {
            "description": "The request cannot be processed: `invalid_json`, `no_registrants`, `batch_too_large`\n(more than 100) or `webinar_required` (neither `webinar_id` nor `slug`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "no_registrants"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The webinar belongs to another workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden"
                }
              }
            }
          },
          "404": {
            "description": "No active webinar with that id or slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "webinar_not_found"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/api-v1": {
      "get": {
        "tags": [
          "Website analytics"
        ],
        "operationId": "websiteAnalytics",
        "summary": "List tracked sites, or read a site's numbers",
        "description": "One endpoint, picked by `action`:\n\n- `action=get_sites` lists the websites tracked by NexaCloser analytics.\n- `action=get_analytics&site_id=…&days=…` returns visitors, page views, sessions,\n  form conversion rate and the top page for the last `days` days (1 to 90, default 1).\n\nAn organization key sees every site of its workspace. A personal key sees the sites it\ncan see in that workspace.\n",
        "parameters": [
          {
            "name": "action",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "get_sites",
                "get_analytics"
              ]
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Required for `get_analytics`. From `get_sites`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "days",
            "in": "query",
            "description": "For `get_analytics`. Window in days, clamped to 1–90.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The sites, or the site's numbers.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/SitesResponse"
                    },
                    {
                      "$ref": "#/components/schemas/SiteAnalytics"
                    }
                  ]
                },
                "examples": {
                  "get_sites": {
                    "value": {
                      "sites": [
                        {
                          "site_id": "a1b2c3d4",
                          "name": "Main website",
                          "domain": "www.example.com"
                        }
                      ],
                      "user_id": "0e985456-cd0a-4417-acb0-6faffd37b88e"
                    }
                  },
                  "get_analytics": {
                    "value": {
                      "visitors": 12,
                      "pageviews": 14,
                      "sessions": 16,
                      "conversion_rate": null,
                      "top_page": {
                        "url": "https://www.example.com/pricing",
                        "views": 6
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`Missing site_id`, or an unknown `action` (the body lists the available actions).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                },
                "example": {
                  "error": "Missing site_id"
                }
              }
            }
          },
          "401": {
            "description": "Missing, wrong, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                },
                "example": {
                  "error": "Unauthorized"
                }
              }
            }
          },
          "404": {
            "description": "The site does not exist or is not in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                },
                "example": {
                  "error": "Site not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/{slug}": {
      "servers": [
        {
          "url": "https://hook.nexacloser.com",
          "description": "Inbound webhooks"
        }
      ],
      "post": {
        "tags": [
          "Inbound webhooks"
        ],
        "operationId": "sendInboundWebhook",
        "summary": "Send data to an inbound webhook",
        "security": [
          {
            "webhookToken": []
          }
        ],
        "description": "Create the webhook in the app under **Automations → Webhooks**; it gets a URL like\n`https://hook.nexacloser.com/<slug>` and a secret token. Send any JSON or form fields to it.\n\n- Every automation whose trigger is this webhook runs, with your fields available as\n  variables in its steps.\n- If the webhook has **CRM Integration** turned on (auto-create a lead each time), the\n  `phone` / `email` / `full_name` (or `name`) fields, after the webhook's field mapping,\n  become a CRM contact and a lead in the chosen pipeline.\n- The same body sent twice within 2 minutes is accepted once: the repeat returns the\n  first `execution_id` with `deduped: true` and starts nothing.\n\nBodies up to 1 MB. `application/json`, `application/x-www-form-urlencoded` and\n`multipart/form-data` are read; any other body is read as JSON, or kept as `raw` text.\n",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The webhook's slug, from its URL.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              },
              "example": {
                "full_name": "Dana Levi",
                "email": "dana@example.com",
                "phone": "+972501234567",
                "form_name": "Contact us"
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookAccepted"
                },
                "examples": {
                  "accepted": {
                    "value": {
                      "success": true,
                      "execution_id": "whx_8plFFIhUWR",
                      "automations_triggered": 1
                    }
                  },
                  "repeat": {
                    "value": {
                      "success": true,
                      "execution_id": "whx_8plFFIhUWR",
                      "automations_triggered": 0,
                      "deduped": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The webhook has a secret and the `X-Webhook-Token` header is missing or wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                },
                "example": {
                  "error": "Unauthorized"
                }
              }
            }
          },
          "404": {
            "description": "No active webhook with this slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                },
                "example": {
                  "error": "Webhook not found"
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 1 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "payload_too_large"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/mcp": {
      "servers": [
        {
          "url": "https://api.nexacloser.com",
          "description": "MCP endpoint"
        }
      ],
      "post": {
        "tags": [
          "MCP"
        ],
        "operationId": "mcpJsonRpc",
        "summary": "MCP JSON-RPC call",
        "security": [
          {
            "apiKey": []
          },
          {
            "mcpOAuth": [
              "mcp:read"
            ]
          },
          {
            "mcpOAuth": [
              "mcp:write"
            ]
          }
        ],
        "description": "The Model Context Protocol endpoint (streamable HTTP, protocol version `2025-06-18`).\nMost people never call it by hand: add `https://api.nexacloser.com/mcp` to an MCP client\nand it handles this. Methods: `initialize`, `tools/list`, `tools/call`, `ping`.\n\nEvery tool, with its arguments, is listed at https://docs.nexacloser.com/mcp/tools/.\nA tool that fails still answers `200` with `result.isError: true` and the reason as text.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpcRequest"
              },
              "examples": {
                "list": {
                  "summary": "List tools",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "tools/list"
                  }
                },
                "call": {
                  "summary": "Call a tool",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 2,
                    "method": "tools/call",
                    "params": {
                      "name": "nc_find_contact",
                      "arguments": {
                        "email": "dana@example.com"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpcResponse"
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": 2,
                  "result": {
                    "content": [
                      {
                        "type": "text",
                        "text": "{ \"found\": false }"
                      }
                    ],
                    "isError": false
                  }
                }
              }
            }
          },
          "401": {
            "description": "No valid credential.",
            "content": {
              "application/json": {
                "example": {
                  "jsonrpc": "2.0",
                  "id": null,
                  "error": {
                    "code": -32001,
                    "message": "Unauthorized"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "nxa_ API key",
        "description": "`Authorization: Bearer nxa_…`. Create keys in the app under **Settings → API**.\nLive keys start with `nxa_`, test keys with `nxa_test_`.\n"
      },
      "webhookToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Webhook-Token",
        "description": "The secret token of an inbound webhook, shown on the webhook in the app."
      },
      "mcpOAuth": {
        "type": "oauth2",
        "description": "OAuth 2.1 with PKCE (S256) and dynamic client registration, for MCP clients only.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://api.nexacloser.com/oauth/authorize",
            "tokenUrl": "https://api.nexacloser.com/oauth/token",
            "refreshUrl": "https://api.nexacloser.com/oauth/token",
            "scopes": {
              "mcp:read": "Read CRM, marketing and automation data.",
              "mcp:write": "Send messages, create deals, run automations and edit data."
            }
          }
        }
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Requests allowed in the current short window.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests left in the current short window.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "InvalidJson": {
        "description": "The body is not valid JSON.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "invalid_json"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, wrong, revoked or rotated key, a key whose access level does not include this endpoint, or a key whose workspace cannot be resolved.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "unauthorized"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Only POST is accepted.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "method_not_allowed"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Wait `Retry-After` seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RateLimitError"
            },
            "example": {
              "error": "rate_limited",
              "message": "Too many requests",
              "retry_after": 7
            }
          }
        }
      },
      "Internal": {
        "description": "Something failed on our side. Safe to retry with backoff.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "internal"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable, machine-readable code."
          }
        }
      },
      "LegacyError": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Older endpoints return a short English message instead of a code.",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "RateLimitError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "const": "rate_limited"
          },
          "message": {
            "type": "string"
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds to wait."
          }
        }
      },
      "ValidationError": {
        "type": "object",
        "required": [
          "error",
          "details"
        ],
        "properties": {
          "error": {
            "type": "string",
            "const": "validation_failed"
          },
          "details": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "code"
              ],
              "properties": {
                "field": {
                  "type": "string",
                  "description": "The field, with dots for nested fields (e.g. `consent.text`, `custom_fields.visit_type`)."
                },
                "code": {
                  "type": "string",
                  "enum": [
                    "invalid_body",
                    "invalid_type",
                    "email_or_phone_required",
                    "invalid_email",
                    "invalid_phone",
                    "invalid_language",
                    "too_many_tags",
                    "invalid_tag",
                    "too_many_custom_fields",
                    "invalid_custom_field_key",
                    "invalid_custom_field_value",
                    "consent_granted_required",
                    "consent_text_required",
                    "invalid_consent_channel",
                    "invalid_consent_purpose",
                    "invalid_consent_time",
                    "invalid_consent_ip",
                    "field_too_long"
                  ]
                }
              }
            }
          }
        }
      },
      "ContactUpsert": {
        "type": "object",
        "description": "Send `email`, `phone` or both.",
        "anyOf": [
          {
            "required": [
              "email"
            ]
          },
          {
            "required": [
              "phone"
            ]
          }
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The contact's email. Stored lowercase."
          },
          "phone": {
            "type": "string",
            "description": "The contact's phone. E.164 (`+14155550101`) is best; a number without a country code is read with the workspace's country. Must come out as 9 to 15 digits."
          },
          "full_name": {
            "type": "string",
            "description": "Full name. Or send `first_name` and `last_name`."
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "maxLength": 100,
            "description": "A label for where the contact came from, e.g. `Website contact form`. Saved as the contact's source channel, only when the contact is created; an existing contact keeps its own. (The CRM source of an API-created contact is always `api`.)"
          },
          "tags": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "string",
              "maxLength": 50
            },
            "description": "Added to the contact's tags. Never removes a tag."
          },
          "custom_fields": {
            "type": "object",
            "maxProperties": 50,
            "description": "Field key → value. Keys match `^[a-z0-9_]{1,64}$`; values are string, number, boolean or null.\nMerged into the contact's existing values. A key with no field defined in the workspace is\nstill saved and is listed in `warnings` as `unknown_custom_field:<key>`.\n",
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "boolean",
                "null"
              ]
            }
          },
          "preferred_language": {
            "type": "string",
            "enum": [
              "he",
              "en",
              "ru"
            ],
            "description": "Language for messages to this contact. Omit to use the workspace default."
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          },
          "consent": {
            "$ref": "#/components/schemas/ContactConsent"
          }
        }
      },
      "Attribution": {
        "type": "object",
        "description": "Where this visit came from. The first touch is kept forever; the last touch is updated on every call.\nValues are trimmed, up to 2,048 characters. Any other key is ignored and listed in `warnings` as\n`unknown_attribution_key:<key>`.\n",
        "additionalProperties": false,
        "properties": {
          "source_channel": {
            "type": "string",
            "maxLength": 2048
          },
          "utm_source": {
            "type": "string",
            "maxLength": 2048
          },
          "utm_medium": {
            "type": "string",
            "maxLength": 2048
          },
          "utm_campaign": {
            "type": "string",
            "maxLength": 2048
          },
          "utm_content": {
            "type": "string",
            "maxLength": 2048
          },
          "utm_term": {
            "type": "string",
            "maxLength": 2048
          },
          "utm_keyword": {
            "type": "string",
            "maxLength": 2048
          },
          "utm_matchtype": {
            "type": "string",
            "maxLength": 2048
          },
          "campaign_id": {
            "type": "string",
            "maxLength": 2048
          },
          "campaign_name": {
            "type": "string",
            "maxLength": 2048
          },
          "adset_id": {
            "type": "string",
            "maxLength": 2048
          },
          "adset_name": {
            "type": "string",
            "maxLength": 2048
          },
          "ad_id": {
            "type": "string",
            "maxLength": 2048
          },
          "ad_name": {
            "type": "string",
            "maxLength": 2048
          },
          "gclid": {
            "type": "string",
            "maxLength": 2048
          },
          "wbraid": {
            "type": "string",
            "maxLength": 2048
          },
          "gbraid": {
            "type": "string",
            "maxLength": 2048
          },
          "fbclid": {
            "type": "string",
            "maxLength": 2048
          },
          "fbc": {
            "type": "string",
            "maxLength": 2048
          },
          "fbp": {
            "type": "string",
            "maxLength": 2048
          },
          "ctwa_clid": {
            "type": "string",
            "maxLength": 2048
          },
          "msclkid": {
            "type": "string",
            "maxLength": 2048
          },
          "ttclid": {
            "type": "string",
            "maxLength": 2048
          },
          "referrer": {
            "type": "string",
            "maxLength": 2048
          },
          "landing_url": {
            "type": "string",
            "maxLength": 2048
          }
        }
      },
      "ContactConsent": {
        "type": "object",
        "required": [
          "granted"
        ],
        "description": "What the person agreed to. Stored per channel, plus an append-only audit record with the\nwording, its version, the time, the page and the key that sent it. A person who opted out of\na channel earlier stays opted out: that channel comes back `withdrawn` with `skipped: true`.\n",
        "properties": {
          "granted": {
            "type": "boolean",
            "description": "`true` = the person opted in; one record per channel is stored. `false` = nothing is recorded (an unticked box is not an opt-out)."
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "email",
                "sms",
                "whatsapp"
              ]
            },
            "description": "Default `email` when you sent an email, plus `sms` and `whatsapp` when you sent a phone."
          },
          "purpose": {
            "type": "string",
            "enum": [
              "marketing",
              "transactional",
              "service_updates",
              "surveys"
            ],
            "default": "marketing"
          },
          "text": {
            "type": "string",
            "maxLength": 2000,
            "description": "The exact wording the person saw. Required when `granted` is `true`."
          },
          "version": {
            "type": "string",
            "maxLength": 50,
            "description": "Your version label for that wording, e.g. `2026-11-01`."
          },
          "language": {
            "type": "string",
            "enum": [
              "he",
              "en",
              "ru"
            ],
            "description": "Language of the wording. Default is the contact's `preferred_language`, else `en`."
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "When the person agreed. Default is the time of the request. May not be more than 5 minutes in the future."
          },
          "ip": {
            "type": "string",
            "description": "The person's IP address (not your server's). IPv4 or IPv6."
          },
          "user_agent": {
            "type": "string",
            "description": "The person's browser user agent."
          },
          "page_url": {
            "type": "string",
            "description": "The page where they agreed."
          }
        }
      },
      "ConsentOutcome": {
        "type": "object",
        "required": [
          "channel",
          "status"
        ],
        "properties": {
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "sms",
              "whatsapp"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "granted",
              "withdrawn",
              "error"
            ],
            "description": "`granted` = stored. `withdrawn` = an earlier opt-out was kept (`skipped: true`). `error` = this channel could not be stored; the contact itself was saved."
          },
          "skipped": {
            "type": "boolean",
            "description": "`true` when an earlier opt-out was kept and your consent was not applied."
          }
        }
      },
      "ContactUpsertResult": {
        "type": "object",
        "required": [
          "id",
          "created",
          "matched_by",
          "consent",
          "warnings"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The contact id."
          },
          "created": {
            "type": "boolean",
            "description": "`true` = a new contact; `false` = an existing one was updated."
          },
          "matched_by": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "phone",
              "email",
              null
            ],
            "description": "How the existing contact was found; `null` when created."
          },
          "consent": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConsentOutcome"
            },
            "description": "One entry per channel when you sent `consent`; empty otherwise."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Non-blocking notes, e.g. `unknown_custom_field:visit_type`."
          }
        }
      },
      "ContactTestResult": {
        "type": "object",
        "required": [
          "test",
          "valid",
          "would",
          "normalized",
          "warnings"
        ],
        "properties": {
          "test": {
            "type": "boolean",
            "const": true
          },
          "valid": {
            "type": "boolean",
            "const": true
          },
          "would": {
            "type": "string",
            "const": "create_or_update",
            "description": "Test keys never look up existing contacts."
          },
          "normalized": {
            "type": "object",
            "additionalProperties": true,
            "description": "What a live key would send on: `email`, `phone` (E.164), `full_name`, `source`, `tags`, `custom_fields`, `preferred_language`, `attribution` and `consent`."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "RegistrantsRequest": {
        "type": "object",
        "required": [
          "registrants"
        ],
        "properties": {
          "webinar_id": {
            "type": "string",
            "description": "The webinar id (`wbn_…`). Wins over `slug`."
          },
          "slug": {
            "type": "string",
            "description": "The webinar's slug in your workspace."
          },
          "registrants": {
            "description": "One registrant, or an array of up to 100.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/Registrant"
              },
              {
                "type": "array",
                "maxItems": 100,
                "items": {
                  "$ref": "#/components/schemas/Registrant"
                }
              }
            ]
          }
        }
      },
      "Registrant": {
        "type": "object",
        "description": "Send an `email`, a `phone`, or both.",
        "properties": {
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "full_name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "description": "Any format; E.164 is best."
          },
          "phone_country_code": {
            "type": "string",
            "description": "Country calling code when you collect it separately, e.g. `972`."
          },
          "phone_national": {
            "type": "string",
            "description": "The national number when you collect it separately."
          },
          "country": {
            "type": "string"
          },
          "page_url": {
            "type": "string",
            "description": "The landing page URL. Its query string is kept as the registration's URL parameters when `url_parameters` is not sent."
          },
          "url_parameters": {
            "type": "string",
            "description": "The raw query string of the landing page (utm_*, click ids)."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": true,
            "description": "Extra answers from your form, by field key."
          },
          "utm_source": {
            "type": "string"
          },
          "utm_medium": {
            "type": "string"
          },
          "utm_campaign": {
            "type": "string"
          },
          "utm_content": {
            "type": "string"
          },
          "utm_term": {
            "type": "string"
          },
          "referrer": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "description": "Registrant language, e.g. `he` or `en`."
          },
          "timezone": {
            "type": "string",
            "description": "IANA time zone, e.g. `Asia/Jerusalem`."
          },
          "consent": {
            "type": "object",
            "description": "Required (`granted: true`) when the registrant has a phone.",
            "properties": {
              "granted": {
                "type": "boolean"
              },
              "text_shown": {
                "type": "string",
                "description": "The exact wording shown next to the checkbox."
              },
              "source": {
                "type": "string",
                "description": "Where the opt-in was collected, e.g. `landing_page`."
              },
              "at": {
                "type": "string",
                "format": "date-time",
                "description": "When it was given. Default is now."
              }
            }
          }
        }
      },
      "RegistrantResult": {
        "type": "object",
        "required": [
          "index",
          "ok"
        ],
        "properties": {
          "index": {
            "type": "integer",
            "description": "Position in your `registrants` array."
          },
          "ok": {
            "type": "boolean"
          },
          "registration_id": {
            "type": "string"
          },
          "personal_link_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The attendee's personal watch link."
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this person was already registered for the session."
          },
          "error": {
            "type": "string",
            "description": "When `ok` is false: `consent_required`, `invalid_registrant`, `email_or_phone_required`,\n`valid email required` (the webinar's form requires an email), `validation_failed`\n(the webinar's form rules), `webinar_full` or `internal`.\n"
          }
        }
      },
      "RegistrantsResponse": {
        "type": "object",
        "required": [
          "results",
          "created",
          "duplicates",
          "failed"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RegistrantResult"
            }
          },
          "created": {
            "type": "integer"
          },
          "duplicates": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          }
        }
      },
      "SitesResponse": {
        "type": "object",
        "properties": {
          "sites": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "site_id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "domain": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "user_id": {
            "type": "string",
            "description": "The user the key acts as."
          }
        }
      },
      "SiteAnalytics": {
        "type": "object",
        "properties": {
          "visitors": {
            "type": "integer"
          },
          "pageviews": {
            "type": "integer"
          },
          "sessions": {
            "type": "integer"
          },
          "conversion_rate": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Form submits ÷ form starts, in percent. `null` when no form was started."
          },
          "top_page": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "url": {
                "type": "string"
              },
              "views": {
                "type": "integer"
              }
            }
          }
        }
      },
      "WebhookAccepted": {
        "type": "object",
        "required": [
          "success",
          "execution_id",
          "automations_triggered"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "execution_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The stored execution, visible on the webhook in the app."
          },
          "automations_triggered": {
            "type": "integer",
            "description": "How many automations were started."
          },
          "deduped": {
            "type": "boolean",
            "description": "`true` when this was a repeat of a body sent in the last 2 minutes."
          }
        }
      },
      "JsonRpcRequest": {
        "type": "object",
        "required": [
          "jsonrpc",
          "method"
        ],
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "integer",
              "string"
            ]
          },
          "method": {
            "type": "string",
            "enum": [
              "initialize",
              "tools/list",
              "tools/call",
              "ping"
            ]
          },
          "params": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "JsonRpcResponse": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "const": "2.0"
          },
          "id": {
            "type": [
              "integer",
              "string",
              "null"
            ]
          },
          "result": {
            "type": "object",
            "additionalProperties": true
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  }
}