Growomat Public API API

Welcome to the **Growomat API**: the same engine that runs the Growomat app, open to your own

GitHub OpenAPI

Operations 120

GET /api/v1/me Introspect the current token #
GET /api/v1/connections List ad platform connections #
GET /api/v1/spaces List your Spaces #
GET /api/v1/openapi.json Download this OpenAPI document #
GET /api/v1/campaigns List campaigns #
POST /api/v1/campaigns Create a campaign #
GET /api/v1/campaigns/{id} Get a campaign #
PUT /api/v1/campaigns/{id} Update a campaign #
DELETE /api/v1/campaigns/{id} Delete a campaign #
POST /api/v1/campaigns/{id}/confirm Confirm a draft campaign #
GET /api/v1/campaigns/{id}/preview Preview a campaign's ads #
POST /api/v1/campaigns/{id}/validate-export Validate a campaign before export #
POST /api/v1/campaigns/{id}/ad-schedule Set the ad schedule #
POST /api/v1/campaigns/{id}/geo-targeting Set geo targeting #
GET /api/v1/ad-groups List ad groups #
POST /api/v1/ad-groups Create an ad group #
GET /api/v1/ad-groups/{id} Get an ad group #
PUT /api/v1/ad-groups/{id} Update an ad group #
DELETE /api/v1/ad-groups/{id} Delete an ad group #
GET /api/v1/ads List ads #
POST /api/v1/ads Create an ad #
GET /api/v1/ads/{id} Get an ad #
PUT /api/v1/ads/{id} Update an ad #
DELETE /api/v1/ads/{id} Delete an ad #
GET /api/v1/keywords List keywords #
POST /api/v1/keywords Create a keyword #
GET /api/v1/keywords/{id} Get a keyword #
PUT /api/v1/keywords/{id} Update a keyword #
DELETE /api/v1/keywords/{id} Delete a keyword #
GET /api/v1/businesses List businesses #
POST /api/v1/businesses Create a business #
GET /api/v1/businesses/{id} Get a business #
PUT /api/v1/businesses/{id} Update a business #
DELETE /api/v1/businesses/{id} Delete a business #
GET /api/v1/assets List assets #
POST /api/v1/assets Create an asset #
POST /api/v1/assets/uploads Start an asset upload #
POST /api/v1/assets/{id}/complete Complete an asset upload #
GET /api/v1/assets/{id} Get an asset #
PUT /api/v1/assets/{id} Update an asset #
DELETE /api/v1/assets/{id} Delete an asset #
GET /api/v1/campaign-assets List campaign asset bindings #
POST /api/v1/campaign-assets Attach an asset to a campaign #
GET /api/v1/campaign-assets/{id} Get a campaign asset binding #
DELETE /api/v1/campaign-assets/{id} Detach an asset from a campaign #
GET /api/v1/ad-assets List ad asset bindings #
POST /api/v1/ad-assets Attach an asset to an ad #
GET /api/v1/ad-assets/{id} Get an ad asset binding #
DELETE /api/v1/ad-assets/{id} Detach an asset from an ad #
GET /api/v1/creative-vocabulary Get the creative vocabulary #
GET /api/v1/angles List angles #
POST /api/v1/angles Create an angle #
GET /api/v1/angles/{id} Get an angle #
PATCH /api/v1/angles/{id} Update an angle #
DELETE /api/v1/angles/{id} Delete an angle #
GET /api/v1/concepts List concepts #
POST /api/v1/concepts Create a concept #
GET /api/v1/concepts/{id} Get a concept #
PATCH /api/v1/concepts/{id} Update a concept #
DELETE /api/v1/concepts/{id} Delete a concept #
GET /api/v1/angles/readout Read out angle results #
GET /api/v1/angles/preflight Estimate time to a verdict #
GET /api/v1/competitors List competitors #
POST /api/v1/competitors Track a competitor #
GET /api/v1/competitors/allowance Get the competitor allowance #
POST /api/v1/competitors/similarity-check Check copy against competitors #
POST /api/v1/competitors/answer-brief Build an answer brief #
GET /api/v1/competitors/{id} Get a competitor #
PATCH /api/v1/competitors/{id} Update a competitor #
DELETE /api/v1/competitors/{id} Stop tracking a competitor #
POST /api/v1/competitors/{id}/identity-candidates Find library accounts #
POST /api/v1/competitors/{id}/identities Confirm a library account #
POST /api/v1/competitors/{id}/identities/reject Reject a library account #
GET /api/v1/competitors/{id}/ads List a competitor's ads #
GET /api/v1/competitors/{id}/offers Get a competitor's offers #
GET /api/v1/competitors/{id}/auction Get auction data for a competitor #
POST /api/v1/competitors/{id}/refresh Refresh a competitor now #
GET /api/v1/businesses/{id}/competitor-coverage Get competitor data coverage #
GET /api/v1/businesses/{id}/angle-gap Get the angle gap #
GET /api/v1/businesses/{id}/own-reading Get what your ads argue #
POST /api/v1/businesses/{id}/own-reading/confirm Confirm what your ads argue #
POST /api/v1/businesses/{id}/auction-imports Upload an auction insights export #
POST /api/v1/businesses/{id}/competitor-suggestions Suggest competitors #
GET /api/v1/media/matrix Get the media requirements matrix #
GET /api/v1/growth-manager/briefing/latest Get the latest briefing #
GET /api/v1/growth-manager/open-loops List the open loops #
GET /api/v1/reports/stats Get account-wide stats #
GET /api/v1/reports/disapprovals List disapproved ads and keywords #
GET /api/v1/reports/timeseries Get a performance time series #
GET /api/v1/reports/breakdown Get a performance breakdown #
POST /api/v1/conversions Upload offline conversions #
GET /api/v1/conversions/status Get offline conversion status #
POST /api/v1/identify Identify a visitor #
POST /api/v1/conversions/events Report conversion events #
POST /api/v1/conversions/adjustments Upload conversion adjustments #
GET /api/v1/conversions/health Get conversion tracking health #
GET /api/v1/conversion-sources List conversion sources #
POST /api/v1/conversion-sources Connect a conversion source #
GET /api/v1/conversion-actions List conversion actions #
POST /api/v1/conversion-actions Create a conversion action #
GET /api/v1/conversion-actions/{id} Get a conversion action #
PUT /api/v1/conversion-actions/{id} Update a conversion action #
DELETE /api/v1/conversion-actions/{id} Delete a conversion action #
GET /api/v1/budget Get the budget and brake state #
PUT /api/v1/budget Set the budget #
GET /api/v1/budget/brake Get the Emergency Brake status #
POST /api/v1/budget/brake/release Release the Emergency Brake #
POST /api/v1/sync Trigger a sync #
GET /api/v1/sync List recent syncs #
GET /api/v1/sync/plan Preview a sync plan #
GET /api/v1/sync/{id} Get a sync #
POST /api/v1/sync/{id}/cancel Cancel a sync #
GET /api/v1/content List content drafts #
POST /api/v1/content Create a content draft #
GET /api/v1/content/{id} Get a content draft #
GET /api/v1/content/{id}/export Export a finished piece #
POST /api/v1/content/{id}/answer Answer an interview question #
POST /api/v1/content/{id}/retry Retry a content draft #
POST /api/v1/content/{id}/confirm Confirm a content draft #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/growomat-com:growomat-public-api-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

growomat-com-api-openapi.json Raw ↑
{
  "openapi": "3.1.0",
  "info": {
    "title": "Growomat Public API",
    "version": "3.1.0",
    "description": "Welcome to the **Growomat API**: the same engine that runs the Growomat app, open to your own\ncode and to AI agents.\n\nEverything Growomat does across **Google Ads**, **Microsoft Advertising**, **Reddit Ads**, **Meta Ads**, **ChatGPT Ads** and **LinkedIn Ads** is here. Create businesses, campaigns,\nad groups, ads and keywords; attach images and video; set schedules and locations; push it all\nlive with a sync; then read back performance, delivery status and conversions. This is the\nstable, versioned public surface at `/api/v1`.\n\n## Base URL\n\nEvery path in this reference is shown in full (`/api/v1/…`) and is served from:\n\n```\nhttps://api.growomat.com\n```\n\n## Authentication\n\nIn the app, open **Tools → API** (`/developers/api`) and create a personal access token. Give it\na name, an expiry of up to one year and only the scopes it needs. The token is shown once and\nlooks like `grow_…`. Send it as a bearer token on every request:\n\n```\nAuthorization: Bearer grow_your_token\n```\n\nEach operation names the scope it needs (`campaigns:read`, `sync:write`, and so on). A token\nwithout that scope gets `403 INSUFFICIENT_SCOPE`. Tokens work only on `/api/v1`.\n\n**Website keys** (`grow_pub_…`) are a second, much narrower credential for code that runs in a\nbrowser. They carry no scopes and reach only *Identify a visitor* and *Report conversion\nevents*; every other operation refuses them with `401`.\n\n## Spaces\n\nYour campaigns live in **Spaces**: one personal Space, plus any shared Spaces you belong to.\nEvery operation runs inside one Space. Send `X-Space-Id: spc_…` to choose it, or omit the header\nfor your personal Space. *List your Spaces* returns the ids. Ad platform connections belong to\nthe account, not the Space, so any Space can sync to any connected platform.\n\n## How a campaign goes live\n\nNothing you create through this API spends money until you sync it.\n\n1. Check *List ad platform connections*: a platform must be `ready` to sync to, and budgets are\n   judged in its account currency.\n2. Create a business, then a campaign (it starts `PAUSED`) with `_platformBudgets` saying where\n   it runs, then its ad groups, ads and keywords. Platform-only settings go in `_native`.\n   A campaign created as `DRAFT` is held back until *Confirm a draft campaign*.\n3. Upload the images the formats need (*Create an asset* takes the bytes inline) and attach\n   them with *Ad assets* or *Campaign assets*.\n4. Set the schedule (*Set the ad schedule*) and the locations (*Set geo targeting*).\n5. Call *Validate a campaign before export* and fix every `requirement` finding; *Preview a\n   campaign's ads* shows each ad as it will be sent.\n6. Call *Preview a sync plan* to see what would be imported, exported and removed. Keep its\n   `planHash`.\n7. Call *Trigger a sync* with that `planHash`. It returns a `syncId` at once.\n8. Poll *Get a sync* until `effectiveStatus` is `completed`, `partial`, `failed` or `cancelled`.\n9. When you want the campaign to serve, set its `status` to `ENABLED` and sync again.\n\nFrom then on *Get a campaign* tells you whether it is serving (`deliverySummary`) and, when it\nis not, why (`reasons[]`).\n\n## Responses and errors\n\nEvery success is wrapped:\n\n```json\n{ \"data\": { … }, \"meta\": { \"nextCursor\": \"…\", \"total\": 42 } }\n```\n\n`meta` appears on list responses. Every error uses one shape:\n\n```json\n{ \"error\": { \"code\": \"NOT_FOUND\", \"message\": \"Campaign not found\", \"details\": { … } } }\n```\n\n| Status | `error.code` | Meaning |\n| --- | --- | --- |\n| 400 | `VALIDATION_ERROR` | A field is wrong; `details.issues[]` names each one. |\n| 400 | `INVALID_REQUEST` | The body is not valid JSON or has the wrong shape. |\n| 401 | `INVALID_TOKEN` | Missing, invalid, expired or revoked token. |\n| 402 | `LIMIT_REACHED` | The plan does not include this, or its monthly allowance is spent. |\n| 403 | `INSUFFICIENT_SCOPE` | The token lacks the scope this operation needs. |\n| 403 | `READ_ONLY_MODE` | The plan can look at everything and change nothing. |\n| 403 | `NOT_A_MEMBER` | You are not a member of the Space in `X-Space-Id`. |\n| 404 | `NOT_FOUND` | No such resource in the active Space. |\n| 404 | `SPACE_NOT_FOUND` | `X-Space-Id` names a Space that does not exist. |\n| 409 | *varies* | The request conflicts with current state; see the operation. |\n| 429 | `RATE_LIMITED` | Too many requests; wait for `Retry-After`. |\n\nOperation-specific codes (for example `SYNC_ALREADY_ACTIVE`) are listed under **Errors** on each\noperation.\n\n## Pagination\n\nList operations take `limit` (default 50, maximum 200) and `cursor`. Follow `meta.nextCursor`\nuntil it is `null`.\n\n## Rate limits\n\nEvery account has a daily request allowance that depends on its plan, shared by all of its\ntokens and reset at midnight UTC, plus a short per-minute burst limit. Past either, requests\nreturn `429 RATE_LIMITED` with a `Retry-After` header. Website keys have their own, separate\nallowance.\n\n## Official SDKs\n\nTyped clients are generated from this exact document, so they cannot drift from the API.\n\n- **TypeScript**: `npm install @defoai/growomat`\n- **Python**: `pip install growomat` (alpha)\n- **Go, PHP and Java**: on the way.\n\n```ts\nimport { Growomat } from '@defoai/growomat';\n\nconst growomat = new Growomat({ token: process.env.GROWOMAT_TOKEN });\nawait growomat.campaigns.create({ name: 'Spring Launch', budget: 50 });\n```\n\nEvery operation below shows a snippet per language. For languages still on the way, the snippet\nshows the shape the client will have.\n\n## For AI agents\n\nIf you are an agent working from this document:\n\n- Start with *Introspect the current token* to learn your scopes and the account time zone.\n- Take ids from list operations; never guess them. Platform ids are exactly the values in the\n  platform enums of this document.\n- Prefer the dry-run forms: `dry_run: true` on schedules and geo targeting, *Preview a sync plan*\n  before *Trigger a sync*, and `planHash` to bind the sync to what you previewed.\n- A `409` is a decision, not a retry. Read `error.code` and the operation's **Errors** list.\n- When a campaign is not serving, relay `deliverySummary.reasons[]`; it is the explanation.\n\n## Versioning\n\n`/api/v1` is stable. New fields, operations and enum values arrive without a change to the path;\n`info.version` records them. Ignore fields you do not know."
  },
  "servers": [
    {
      "url": "https://api.growomat.com",
      "description": "Production"
    },
    {
      "url": "https://{host}",
      "description": "Environment-templated base URL",
      "variables": {
        "host": {
          "default": "api.growomat.com",
          "enum": [
            "api.growomat.com",
            "api-ads-staging.defoai.com"
          ],
          "description": "api.growomat.com is production; api-ads-staging.defoai.com is staging."
        }
      }
    }
  ],
  "tags": [
    {
      "name": "meta",
      "x-displayName": "Start here",
      "description": "Check your token, find your Spaces, see which ad platforms are connected and download this document."
    },
    {
      "name": "businesses",
      "x-displayName": "Businesses",
      "description": "The advertiser: its website, what it does and its brand voice. Campaigns belong to a business, and every piece of generated copy is written for one."
    },
    {
      "name": "campaigns",
      "x-displayName": "Campaigns",
      "description": "The unit of budget, targeting and delivery. A campaign is created locally and reaches the ad platforms only when you sync it. Delivery status on every campaign tells you whether it is serving and, if not, why."
    },
    {
      "name": "ad-groups",
      "x-displayName": "Ad groups",
      "description": "Groups of ads and keywords inside a campaign, with a default bid."
    },
    {
      "name": "ads",
      "x-displayName": "Ads",
      "description": "The creative: headlines, descriptions, the landing page, and extensions such as sitelinks and callouts."
    },
    {
      "name": "keywords",
      "x-displayName": "Keywords",
      "description": "The search terms an ad group bids on, with match type and optional negative keywords."
    },
    {
      "name": "assets",
      "x-displayName": "Assets",
      "description": "Images, logos and videos, uploaded or registered by URL, and attached to campaigns, ad groups or ads."
    },
    {
      "name": "campaign-assets",
      "x-displayName": "Campaign assets",
      "description": "Which asset plays which role in a campaign."
    },
    {
      "name": "ad-assets",
      "x-displayName": "Ad assets",
      "description": "Which asset plays which role in an ad or ad group; the level display, video and Performance Max formats read from."
    },
    {
      "name": "media",
      "x-displayName": "Media requirements",
      "description": "What each platform and campaign type needs before it can serve."
    },
    {
      "name": "angles",
      "x-displayName": "Angles and concepts",
      "description": "Angles are the claims your ads argue; concepts are ways of making a claim. Stamp ads with them and Growomat reports which claim wins, and how long a test will take."
    },
    {
      "name": "competitors",
      "x-displayName": "Competitor research",
      "description": "The competitors of a business: what they publish in the public ad libraries, which claims and offers they lead with, and where they beat you in your own auctions. Library data shows what was published, never what worked."
    },
    {
      "name": "sync",
      "x-displayName": "Sync",
      "description": "The only way anything reaches an ad platform. Preview the plan, trigger the sync, poll the record, and unlink when you need a clean slate."
    },
    {
      "name": "budget",
      "x-displayName": "Budget and Emergency Brake",
      "description": "The account budget and the brake that pauses campaigns automatically when real spend passes it."
    },
    {
      "name": "growth-manager",
      "x-displayName": "Growth manager",
      "description": "What the growth manager last told the Space, and what it still has to report back on. Read-only."
    },
    {
      "name": "reports",
      "x-displayName": "Reports",
      "description": "Performance over time and per entity, account totals, and what the platforms currently refuse to show."
    },
    {
      "name": "conversions",
      "x-displayName": "Conversions",
      "description": "Tell the ad platforms what your clicks turned into: offline conversions, first-party identities, conversion events from your stack, adjustments, and the health of the loop."
    },
    {
      "name": "content",
      "x-displayName": "Social content",
      "description": "Research-grounded social posts, drafted for a business, interviewed, reviewed and confirmed."
    }
  ],
  "x-tagGroups": [
    {
      "name": "Start here",
      "tags": [
        "meta"
      ]
    },
    {
      "name": "Campaign structure",
      "tags": [
        "businesses",
        "campaigns",
        "ad-groups",
        "ads",
        "keywords"
      ]
    },
    {
      "name": "Creative and media",
      "tags": [
        "assets",
        "campaign-assets",
        "ad-assets",
        "media",
        "angles"
      ]
    },
    {
      "name": "Go live",
      "tags": [
        "sync",
        "budget"
      ]
    },
    {
      "name": "Measure",
      "tags": [
        "reports",
        "conversions",
        "growth-manager"
      ]
    },
    {
      "name": "Market research",
      "tags": [
        "competitors"
      ]
    },
    {
      "name": "Social content",
      "tags": [
        "content"
      ]
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Growomat API Token (grow_...)",
        "description": "A personal access token created in the app under **Tools → API** (`/developers/api`). Carries the scopes it was created with."
      },
      "PublishableKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Growomat website key (grow_pub_...)",
        "description": "The credential that is safe to put in a public web page. It carries no scopes: its capability is the credential class itself, and it reaches exactly two operations, *Identify a visitor* and *Report conversion events*. Every other operation, including every read, refuses it with the same `401` an unknown credential gets.\n\nA key is bound to one Space and, optionally, to a list of websites it may be used from. It is metered on its own allowance and never draws on the account request quota.\n\nOn *Report conversion events* a website key runs a constrained form: the source is set by the server, adjustments are refused, conversion actions are never created, and the batch is capped. Sales it reports are recorded as unverified claims, visible to the account owner, never sent to an ad platform and never counted in a report, until a server-side source confirms the same `orderId`."
      }
    },
    "schemas": {
      "SuccessEnvelope": {
        "type": "object",
        "required": [
          "data"
        ],
        "description": "The shape of every successful response.",
        "properties": {
          "data": {
            "description": "The result: an object for single resources, an array for lists."
          },
          "meta": {
            "type": "object",
            "description": "Present on list responses.",
            "properties": {
              "nextCursor": {
                "type": [
                  "string",
                  "number",
                  "null"
                ],
                "description": "Pass as `cursor` to get the next page. `null` on the last page."
              },
              "total": {
                "type": "integer",
                "description": "Number of items in this page."
              }
            }
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "The shape of every error response.",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code, for example `NOT_FOUND`. Safe to branch on."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation. May change; do not parse it."
              },
              "details": {
                "description": "Extra context, when there is any: validation issues, conflicting ids, and so on."
              }
            }
          }
        }
      },
      "LimitReachedBody": {
        "type": "object",
        "required": [
          "error",
          "reason",
          "action"
        ],
        "description": "The body of a `402`: a plan limit was hit. `reason` says which kind, because the four kinds call for different fixes.",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "LIMIT_REACHED"
            ]
          },
          "reason": {
            "type": "string",
            "enum": [
              "NOT_IN_PLAN",
              "QUOTA_SPENT",
              "SUBSCRIPTION_EXPIRED",
              "PLAN_UNAVAILABLE"
            ],
            "description": "`QUOTA_SPENT`: the allowance exists, has been used up, and comes back; this is the only value with a real `cap`, `currentUsage` and `resetAt`. `NOT_IN_PLAN`: the plan does not include this capability at all, so those three fields are `null`; there is nothing to wait for. `SUBSCRIPTION_EXPIRED`: the subscription lapsed; renewing restores it. `PLAN_UNAVAILABLE`: the account references a plan that no longer exists; contact support, no purchase resolves it."
          },
          "action": {
            "type": "string",
            "description": "The metered action that was refused, for example `content_draft`."
          },
          "cap": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The allowance. `0` means unlimited; `null` means there is no allowance to report. The two are not the same."
          },
          "currentUsage": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How much of the allowance has been used this period."
          },
          "capField": {
            "type": [
              "string",
              "null"
            ],
            "description": "The plan field this action is metered against, for example `monthlyResearchQueries`, so a client can find the cheapest plan that raises this particular allowance."
          },
          "resetAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "When the allowance refills, as epoch milliseconds. `null` for every reason except `QUOTA_SPENT`."
          },
          "upgradeUrl": {
            "type": "string",
            "description": "Where to change the plan."
          }
        }
      },
      "ScheduleWindow": {
        "type": "object",
        "required": [
          "day",
          "startHour",
          "endHour"
        ],
        "description": "One block of time on one day of the week during which ads may show.",
        "properties": {
          "day": {
            "type": "string",
            "enum": [
              "MONDAY",
              "TUESDAY",
              "WEDNESDAY",
              "THURSDAY",
              "FRIDAY",
              "SATURDAY",
              "SUNDAY"
            ],
            "description": "The day of the week."
          },
          "startHour": {
            "type": "integer",
            "minimum": 0,
            "maximum": 23,
            "description": "Hour the window opens (0 to 23), in the schedule's time zone."
          },
          "startMinute": {
            "type": "string",
            "enum": [
              "ZERO",
              "FIFTEEN",
              "THIRTY",
              "FORTY_FIVE"
            ],
            "default": "ZERO",
            "description": "Minute the window opens, in quarter hours."
          },
          "endHour": {
            "type": "integer",
            "minimum": 0,
            "maximum": 24,
            "description": "Hour the window closes (0 to 24). `24` means end of day."
          },
          "endMinute": {
            "type": "string",
            "enum": [
              "ZERO",
              "FIFTEEN",
              "THIRTY",
              "FORTY_FIVE"
            ],
            "default": "ZERO",
            "description": "Minute the window closes, in quarter hours."
          },
          "bidModifier": {
            "type": "number",
            "minimum": 0.1,
            "maximum": 10,
            "description": "Bid multiplier for this window; `1.0` means no change. Platforms that express this as a percentage receive the converted value, clamped to their range; a clamp is reported as a `BID_ADJUSTMENT_CLAMPED` warning."
          }
        }
      },
      "StructuredSchedule": {
        "type": "object",
        "required": [
          "windows"
        ],
        "description": "A weekly ad schedule: up to 84 windows and the time zone they are in.",
        "properties": {
          "windows": {
            "type": "array",
            "maxItems": 84,
            "items": {
              "$ref": "#/components/schemas/ScheduleWindow"
            },
            "description": "The windows during which ads may show."
          },
          "timezone": {
            "type": "string",
            "description": "IANA time zone the hours are in, for example `Europe/Berlin`. Defaults to the account time zone."
          }
        }
      },
      "CampaignAdScheduleField": {
        "description": "The campaign's ad schedule, as a preset name (`ALL_HOURS_ALL_DAYS`, `BUSINESS_HOURS`, `CUSTOM`) or as a `StructuredSchedule`. On read, `adSchedule` returns what you last wrote and `adScheduleStructured` always holds the expanded weekly schedule. `CUSTOM` is treated as `ALL_HOURS_ALL_DAYS`.",
        "oneOf": [
          {
            "type": "string",
            "enum": [
              "ALL_HOURS_ALL_DAYS",
              "BUSINESS_HOURS",
              "CUSTOM"
            ]
          },
          {
            "$ref": "#/components/schemas/StructuredSchedule"
          }
        ]
      },
      "SetAdScheduleWindow": {
        "type": "object",
        "required": [
          "days",
          "start",
          "end"
        ],
        "description": "A window in human terms: which days, from when, until when.",
        "properties": {
          "days": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Day tokens: `MON` to `SUN`, full day names, `WEEKDAYS` or `WEEKEND`."
          },
          "start": {
            "type": "string",
            "description": "Start time as `HH:MM`. Minutes must be `00`, `15`, `30` or `45`."
          },
          "end": {
            "type": "string",
            "description": "End time as `HH:MM`; `24:00` is allowed. An end at or before the start means the window crosses midnight and is split automatically."
          },
          "bidModifier": {
            "type": "number",
            "minimum": 0.1,
            "maximum": 10,
            "description": "Bid multiplier for this window; `1.0` means no change."
          }
        }
      },
      "SetAdSchedule": {
        "type": "object",
        "description": "Human-friendly ad schedule input. Provide a preset and/or windows; the server expands them, splits windows that cross midnight, resolves the time zone and stores the result as `adScheduleStructured`.",
        "properties": {
          "timezone": {
            "type": "string",
            "description": "`local` (the searcher's own time zone where the platform supports it, otherwise account time with a warning), `UTC`, or an IANA zone such as `Europe/Berlin`. A zone that differs from the account zone is converted into account-time hours before storing."
          },
          "windows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SetAdScheduleWindow"
            },
            "description": "The windows to set."
          },
          "preset": {
            "type": "string",
            "enum": [
              "ALL_HOURS_ALL_DAYS",
              "BUSINESS_HOURS",
              "OVERNIGHT",
              "WEEKENDS",
              "PHARMACY_OPEN_HOURS"
            ],
            "description": "A ready-made schedule. Can be combined with `windows`."
          },
          "platform": {
            "type": "string",
            "enum": [
              "google-ads",
              "microsoft-ads",
              "reddit-ads",
              "meta-ads",
              "openai-ads",
              "linkedin-ads"
            ],
            "description": "Which platform's time zone rules to apply when the campaign runs on several. Inferred when omitted."
          },
          "dry_run": {
            "type": "boolean",
            "description": "When `true`, return the expanded schedule and warnings without changing the campaign."
          }
        }
      },
      "SetGeoTargetingArea": {
        "description": "A named place, as a plain string or as an object with hints for resolving it.",
        "oneOf": [
          {
            "type": "string",
            "description": "A place name, for example \"Germany\", \"Berlin\" or \"Mitte\"."
          },
          {
            "type": "object",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "description": "The place name."
              },
              "type": {
                "type": "string",
                "description": "What kind of place to look for, for example `CITY` or `DISTRICT`."
              },
              "countryCode": {
                "type": "string",
                "description": "ISO country code to search within."
              }
            }
          }
        ]
      },
      "SetGeoTargetingRadius": {
        "type": "object",
        "required": [
          "radius"
        ],
        "description": "A circle around a point, given by coordinates or by a place name.",
        "properties": {
          "around": {
            "type": "string",
            "description": "Place name to centre on; resolved to coordinates on the server. Alternative to `lat` and `lng`."
          },
          "lat": {
            "type": "number",
            "minimum": -90,
            "maximum": 90,
            "description": "Latitude of the centre."
          },
          "lng": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Longitude of the centre."
          },
          "radius": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Radius of the circle."
          },
          "unit": {
            "type": "string",
            "enum": [
              "km",
              "mi"
            ],
            "default": "km",
            "description": "Unit of `radius`."
          },
          "name": {
            "type": "string",
            "description": "Display label for the circle."
          },
          "countryCode": {
            "type": "string",
            "description": "ISO country code to resolve `around` within."
          }
        }
      },
      "SetGeoTargeting": {
        "type": "object",
        "description": "Human-friendly location input. Provide the structured `geoTargets` array, or `include`, `exclude` and `radii` lists of place names that the server resolves, or only `locationIntent` to change that alone. `dry_run: true` returns the resolved rows and a plain-language sentence describing the effective targeting, without changing the campaign.",
        "properties": {
          "geoTargets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GeoTarget"
            },
            "description": "The full structured list, replacing the current one."
          },
          "include": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SetGeoTargetingArea"
            },
            "description": "Places to show ads in."
          },
          "exclude": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SetGeoTargetingArea"
            },
            "description": "Places to carve out of the included set."
          },
          "radii": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SetGeoTargetingRadius"
            },
            "description": "Circles to show ads in."
          },
          "locationIntent": {
            "type": "string",
            "enum": [
              "PRESENCE",
              "PRESENCE_OR_INTEREST"
            ],
            "description": "Who counts as being in the targeted places. See `CampaignCreate.locationIntent`."
          },
          "dry_run": {
            "type": "boolean",
            "description": "When `true`, preview the resolved set and the sentence without changing the campaign."
          }
        }
      },
      "GeoTarget": {
        "type": "object",
        "required": [
          "kind",
          "name"
        ],
        "description": "One row of a campaign's location targeting: a named area or a radius circle, included or excluded.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable row id. Generated when omitted."
          },
          "kind": {
            "type": "string",
            "enum": [
              "AREA",
              "RADIUS"
            ],
            "description": "`AREA` is a named place, from a country down to a postcode. `RADIUS` is a circle around a point."
          },
          "negative": {
            "type": "boolean",
            "default": false,
            "description": "`true` excludes this row from the included set. Not allowed on `RADIUS` rows (`400 RADIUS_EXCLUSION_UNSUPPORTED`), and a set where every row is negative is rejected (`400 ONLY_EXCLUSIONS`)."
          },
          "bidAdjustment": {
            "type": "number",
            "description": "Percent bid adjustment for this row. Included rows only."
          },
          "reach": {
            "type": "number",
            "description": "Audience estimate as reported by the platform, when it gave one. Read-only; never estimated by Growomat."
          },
          "criterionId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Areas: the Google Ads geo target id."
          },
          "msLocationId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Areas: the Microsoft Advertising location id."
          },
          "canonicalId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Areas: Growomat's platform-independent id for the place."
          },
          "name": {
            "type": "string",
            "description": "Display name of the place or circle."
          },
          "type": {
            "type": "string",
            "enum": [
              "COUNTRY",
              "REGION",
              "METRO",
              "CITY",
              "DISTRICT",
              "POSTAL_CODE",
              "AIRPORT",
              "UNIVERSITY",
              "GLOBAL"
            ],
            "description": "Areas only: what kind of place this is."
          },
          "countryCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Areas: ISO 3166-1 country code."
          },
          "lat": {
            "type": "number",
            "minimum": -90,
            "maximum": 90,
            "description": "Circles only: latitude of the centre."
          },
          "lng": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Circles only: longitude of the centre."
          },
          "radius": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Circles only. Clamped to the platform's range (1 to 500) when pushed; a clamp is reported as a `RADIUS_CLAMPED_TO_MIN` warning."
          },
          "unit": {
            "type": "string",
            "enum": [
              "km",
              "mi"
            ],
            "default": "km",
            "description": "Circles only: the unit of `radius`."
          }
        }
      },
      "BiddingStrategy": {
        "type": "object",
        "description": "How the platform bids for this campaign.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "MANUAL_CPC",
              "ENHANCED_CPC",
              "MAXIMIZE_CLICKS",
              "MAXIMIZE_CONVERSIONS",
              "MAXIMIZE_CONVERSION_VALUE",
              "TARGET_CPA",
              "TARGET_ROAS",
              "TARGET_IMPRESSION_SHARE"
            ],
            "default": "MAXIMIZE_CLICKS",
            "description": "The bidding strategy. Target strategies read their target from the matching field below."
          },
          "targetCpa": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Target cost per acquisition, for `TARGET_CPA`."
          },
          "targetRoas": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Target return on ad spend as a ratio (2.5 means 250%), for `TARGET_ROAS`."
          },
          "targetImpressionShare": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "d

# --- truncated at 32 KB (708 KB total) ---
# Full source: https://artifacts.apis.io/growomat-com/openapi/_original/growomat-com-api-openapi.json