{
  "openapi": "3.1.0",
  "info": {
    "title": "Zoekworden Campaign API",
    "version": "v1",
    "description": "REST API for AI assistants to read keyword trends, campaign performance and recommendations, and to create Google Ads campaigns in a safe, human-approved workflow. Campaigns are always created paused with a budget ceiling; a human enables them in the Google Ads UI."
  },
  "servers": [
    {
      "url": "https://zoekworden.nl/api"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer"
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key"
      }
    },
    "schemas": {
      "CampaignCreateRequest": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "title": "CampaignCreateRequest",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "daily_budget_eur",
          "budget_cap_eur",
          "keywords"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 3,
            "maxLength": 255
          },
          "daily_budget_eur": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 100000,
            "description": "Daily budget in EUR. Must be <= budget_cap_eur."
          },
          "budget_cap_eur": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 100000,
            "description": "Hard ceiling set by the account owner, in EUR."
          },
          "target_country": {
            "type": "string",
            "pattern": "^[a-zA-Z]{2}$",
            "default": "nl",
            "description": "ISO-3166 alpha-2 country code."
          },
          "keywords": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "description": "Each item is either a string, or an object {\"text\": ..., \"match_type\": broad|phrase|exact}. A plain string defaults to phrase match.",
            "items": {
              "oneOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 80
                },
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "text"
                  ],
                  "properties": {
                    "text": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 80
                    },
                    "match_type": {
                      "type": "string",
                      "enum": [
                        "broad",
                        "phrase",
                        "exact"
                      ]
                    }
                  }
                }
              ]
            }
          },
          "negative_keywords": {
            "type": "array",
            "maxItems": 200,
            "description": "Optional. Words that must NOT trigger the ads, protecting the budget from irrelevant queries. Same item shape as keywords.",
            "items": {
              "oneOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 80
                },
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "text"
                  ],
                  "properties": {
                    "text": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 80
                    },
                    "match_type": {
                      "type": "string",
                      "enum": [
                        "broad",
                        "phrase",
                        "exact"
                      ]
                    }
                  }
                }
              ]
            }
          },
          "dry_run": {
            "type": "boolean",
            "description": "If true, validate and return the resolved plan without creating anything. Use it to preview exactly what will be built."
          },
          "final_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 500,
            "description": "Landing page URL for the ads."
          }
        }
      },
      "ConnectionLinkRequest": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "title": "ConnectionLinkRequest",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "customer_id"
        ],
        "properties": {
          "customer_id": {
            "type": "string",
            "minLength": 10,
            "maxLength": 12,
            "description": "The user's own Google Ads customer id, 10 digits, dashes optional (e.g. 123-456-7890). Shown in the top-right corner of ads.google.com. This is the ONLY place an account id is ever accepted from a client."
          }
        }
      }
    }
  },
  "security": [
    {
      "bearer": []
    },
    {
      "apiKeyHeader": []
    }
  ],
  "paths": {
    "/v1/me": {
      "get": {
        "summary": "Identify the key, its scope, and the Google Ads link state (from our database, no Ads API call)."
      }
    },
    "/v1/connection/status": {
      "get": {
        "summary": "Full setup state: Google Ads link status (live re-check, throttled), readiness checks, and the exact next action for the assistant."
      }
    },
    "/v1/connection/link": {
      "post": {
        "summary": "Link the key owner's own Google Ads account: sends a manager invitation from Zoekworden which an ADMIN of that account accepts inside Google Ads. Write scope required. Idempotent - repeat POSTs refresh the state; a different customer_id replaces the link.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectionLinkRequest"
              }
            }
          }
        }
      }
    },
    "/v1/keywords/trends": {
      "get": {
        "summary": "Keyword metrics and daily search-volume series.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "nl"
            }
          },
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 30,
              "maximum": 90
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ]
      }
    },
    "/v1/campaigns": {
      "get": {
        "summary": "List the key owner's campaigns."
      },
      "post": {
        "summary": "Create a PAUSED campaign, or preview one with dry_run=true. Write scope; Idempotency-Key header required for a real create (not for a preview).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CampaignCreateRequest"
              }
            }
          }
        }
      }
    },
    "/v1/campaigns/{id}": {
      "get": {
        "summary": "Campaign detail with daily ROAS/ACOS series."
      }
    },
    "/v1/usage": {
      "get": {
        "summary": "API usage and remaining quota."
      }
    },
    "/v1/recommendations": {
      "get": {
        "summary": "Current optimisation recommendations."
      }
    },
    "/v1/recommendations/{id}/dismiss": {
      "post": {
        "summary": "Dismiss a recommendation so it is not offered again."
      }
    }
  }
}