{
  "openapi": "3.1.0",
  "info": {
    "title": "OpenContact API",
    "version": "1.0.0",
    "description": "Capture a contact once, route it to the email and CRM tools you use, and keep suppression state in sync. All endpoints are project-scoped and authenticated with a bearer API key.",
    "contact": { "name": "OpenContact", "url": "https://app.opencontact.dev" }
  },
  "servers": [{ "url": "https://app.opencontact.dev/api/v1" }],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Contacts" },
    { "name": "Lists" },
    { "name": "Events" },
    { "name": "Rules" },
    { "name": "Webhooks" }
  ],
  "paths": {
    "/projects/{project}/contacts": {
      "parameters": [{ "$ref": "#/components/parameters/project" }],
      "get": {
        "tags": ["Contacts"],
        "summary": "List contacts",
        "description": "Paginated and filterable. Pass updated_since (ISO-8601) for incremental sync (results are oldest-first).",
        "security": [{ "bearerAuth": ["lists:read"] }],
        "parameters": [
          { "name": "status", "in": "query", "schema": { "$ref": "#/components/schemas/Status" } },
          { "name": "email", "in": "query", "schema": { "type": "string", "format": "email" } },
          { "name": "search", "in": "query", "schema": { "type": "string" } },
          { "name": "updated_since", "in": "query", "schema": { "type": "string", "format": "date-time" } },
          { "name": "per_page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1 } }
        ],
        "responses": {
          "200": {
            "description": "A page of contacts.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactPage" } } }
          }
        }
      },
      "post": {
        "tags": ["Contacts"],
        "summary": "Upsert a contact",
        "description": "Create or update a contact by normalized email. Send an Idempotency-Key header to make retries safe.",
        "security": [{ "bearerAuth": ["lists:write"] }],
        "parameters": [
          { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactInput" } } }
        },
        "responses": {
          "201": { "description": "Contact created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Contact" } } } },
          "200": { "description": "Existing contact updated.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Contact" } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/projects/{project}/lists": {
      "parameters": [{ "$ref": "#/components/parameters/project" }],
      "get": {
        "tags": ["Lists"], "summary": "List contact lists",
        "security": [{ "bearerAuth": ["contacts:read"] }],
        "responses": { "200": { "description": "The project's lists.", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/List" } } } } } }
      },
      "post": {
        "tags": ["Lists"], "summary": "Create a contact list",
        "security": [{ "bearerAuth": ["contacts:write"] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" }, "description": { "type": "string" } } } } } },
        "responses": { "201": { "description": "List created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/List" } } } } }
      }
    },
    "/projects/{project}/events": {
      "parameters": [{ "$ref": "#/components/parameters/project" }],
      "get": {
        "tags": ["Events"], "summary": "Read the event stream",
        "security": [{ "bearerAuth": ["events:read"] }],
        "parameters": [
          { "name": "type", "in": "query", "schema": { "type": "string" } },
          { "name": "since", "in": "query", "schema": { "type": "string", "format": "date-time" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } }
        ],
        "responses": { "200": { "description": "Recent events, newest first.", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Event" } } } } } }
      }
    },
    "/projects/{project}/rules": {
      "parameters": [{ "$ref": "#/components/parameters/project" }],
      "get": { "tags": ["Rules"], "summary": "List sync rules", "security": [{ "bearerAuth": ["contacts:read"] }], "responses": { "200": { "description": "The project's sync rules." } } },
      "post": { "tags": ["Rules"], "summary": "Create a sync rule", "security": [{ "bearerAuth": ["contacts:write"] }], "responses": { "201": { "description": "Rule created." } } }
    },
    "/projects/{project}/webhooks": {
      "parameters": [{ "$ref": "#/components/parameters/project" }],
      "get": { "tags": ["Webhooks"], "summary": "List webhook subscriptions", "security": [{ "bearerAuth": ["webhooks:manage"] }], "responses": { "200": { "description": "Subscriptions." } } },
      "post": {
        "tags": ["Webhooks"], "summary": "Create a webhook subscription",
        "description": "Returns the signing secret once.",
        "security": [{ "bearerAuth": ["webhooks:manage"] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["url", "events"], "properties": { "url": { "type": "string", "format": "uri" }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/EventType" } } } } } } },
        "responses": { "201": { "description": "Subscription created; secret shown once.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookSubscription" } } } } }
      }
    },
    "/projects/{project}/webhooks/{subscription}": {
      "parameters": [{ "$ref": "#/components/parameters/project" }, { "name": "subscription", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "delete": { "tags": ["Webhooks"], "summary": "Delete a subscription", "security": [{ "bearerAuth": ["webhooks:manage"] }], "responses": { "200": { "description": "Deleted." } } }
    },
    "/projects/{project}/webhooks/{subscription}/test": {
      "parameters": [{ "$ref": "#/components/parameters/project" }, { "name": "subscription", "in": "path", "required": true, "schema": { "type": "integer" } }],
      "post": { "tags": ["Webhooks"], "summary": "Send a test event", "security": [{ "bearerAuth": ["webhooks:manage"] }], "responses": { "200": { "description": "Test event queued." } } }
    }
  },
  "webhooks": {
    "contactEvent": {
      "post": {
        "summary": "Outbound event delivery",
        "description": "OpenContact POSTs to your subscribed URL. Headers: X-EmailRoute-Event, X-EmailRoute-Delivery (stable id), and X-EmailRoute-Signature ('t=<unix_ts>,v1=<hmac_sha256 of \"timestamp.body\" with your secret>'). Reject stale timestamps, then verify v1.",
        "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookDelivery" } } } },
        "responses": { "200": { "description": "Acknowledge with any 2xx; non-2xx is retried with backoff." } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "description": "A project-scoped API key created on the Developers page." }
    },
    "parameters": {
      "project": { "name": "project", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[A-Z0-9]{7}$", "example": "AB12CD3" }, "description": "Seven-character project ID shown on the project dashboard." }
    },
    "schemas": {
      "Status": { "type": "string", "enum": ["subscribed", "unsubscribed", "suppressed", "bounced", "complained", "unknown"] },
      "EventType": { "type": "string", "enum": ["contact.created", "contact.status_changed", "contact.unsubscribed", "contact.bounced", "contact.complained"] },
      "ContactInput": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "first_name": { "type": "string" },
          "last_name": { "type": "string" },
          "phone": { "type": "string" },
          "company": { "type": "string" },
          "country": { "type": "string" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "source": { "type": "string" },
          "status": { "$ref": "#/components/schemas/Status" },
          "list_ids": { "type": "array", "items": { "type": "integer" } },
          "custom_fields": { "type": "object", "additionalProperties": true },
          "consent": { "type": "object", "additionalProperties": true }
        }
      },
      "Contact": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "email": { "type": "string" },
          "first_name": { "type": ["string", "null"] },
          "last_name": { "type": ["string", "null"] },
          "status": { "$ref": "#/components/schemas/Status" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "custom_fields": { "type": "object", "additionalProperties": true },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "ContactPage": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Contact" } },
          "links": { "type": "object" },
          "meta": { "type": "object" }
        }
      },
      "List": {
        "type": "object",
        "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "slug": { "type": "string" } }
      },
      "Event": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "contact_id": { "type": ["integer", "null"] },
          "type": { "type": "string" },
          "source": { "type": "string" },
          "occurred_at": { "type": "string", "format": "date-time" }
        }
      },
      "WebhookSubscription": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "url": { "type": "string", "format": "uri" },
          "events": { "type": "array", "items": { "$ref": "#/components/schemas/EventType" } },
          "secret": { "type": "string", "description": "Signing secret; returned only when the subscription is created." }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Stable delivery id (evt_...), matches X-EmailRoute-Delivery." },
          "event": { "$ref": "#/components/schemas/EventType" },
          "occurred_at": { "type": "string", "format": "date-time" },
          "data": { "$ref": "#/components/schemas/Contact" }
        }
      }
    },
    "responses": {
      "ValidationError": {
        "description": "Validation failed.",
        "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" }, "errors": { "type": "object" } } } } }
      }
    }
  }
}
