REST API

The ads platform as a plain JSON API.

Cursor-paginated REST over campaigns, ad groups, ads, keywords, assets, reports and sync — across Google Ads, Microsoft Advertising, Reddit Ads and Meta Ads. One envelope, typed errors, and a written versioning policy.

Base URL

https://api.growomat.com/api/v1

Spec

GET /api/v1/openapi.json · OpenAPI 3.1

Auth

Authorization: Bearer grow_… · 13 scopes

Limits

60 req/min per token · Retry-After on 429

v1 · additive changes are never breaking · old versions run for at least 12 months

first request — GET /me

LIVE
$ curl -s https://api.growomat.com/api/v1/me \
    -H "Authorization: Bearer $GROWOMAT_TOKEN"

{ "data": { "uid": "user_abc", "email": "[email protected]",
    "plan": { "id": "pro" },
    "scopes": ["campaigns:read", "campaigns:write"] } }
Quickstart · for builders

Zero to a staged campaign in three requests.

Campaigns are created locally and staged — nothing touches a live ad platform until a token holding the sync scope triggers a sync.

1
Mint a token

Settings → Developer

Generate a token with a name, an expiry of up to one year, and the minimum scopes it needs. The plaintext shows once — only its SHA-256 is stored.

# shown once — store it in a secret manager
grow_live_A1B2C3D4E5F6G7H8_veryLongSecret…

export GROWOMAT_TOKEN=grow_live_…
2
Introspect it

Confirm auth with /me

GET /me echoes the token’s identity, plan, and granted scopes — the fastest way to confirm your wiring.

curl -s -H "Authorization: Bearer $GROWOMAT_TOKEN" \
  https://api.growomat.com/api/v1/me

# → { "data": { "uid": "user_abc", "email": "[email protected]",
#     "plan": { "id": "pro", "name": "Pro" },
#     "scopes": ["campaigns:read","campaigns:write"] } }
3
Create something

POST your first campaign

Writes need their resource’s write scope. Every response uses the same envelope: { data, meta }

curl -s -X POST https://api.growomat.com/api/v1/campaigns \
  -H "Authorization: Bearer $GROWOMAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Black Friday 2026","type":"SEARCH","budget":50}'
Resources

Ten resources, one shape.

Every resource speaks the same envelope and the same cursor pagination. What the app can create, list, edit and delete — so can you.

ResourceRoutesOperations
Token/me
GET
Campaigns/campaigns · /campaigns/:id
GETPOSTPATCHDELETE
Ad groups/ad-groups · /ad-groups/:id
GETPOSTPATCHDELETE
Ads/ads · /ads/:id
GETPOSTPATCHDELETE
Keywords/keywords · /keywords/:id
GETPOSTPATCHDELETE
Assets/assets · /assets/:id/attach
GETPOSTPATCHDELETE
Businesses/businesses · /businesses/:id
GETPOSTPATCHDELETE
Reports/reports/performance · /reports/disapprovals
GET
Sync/sync · /sync/preview · /sync/:id
GETPOST
Conversions/conversions/offline
GETPOST

Writes need their resource’s :write scope. Triggering a sync additionally needs sync:write — it is the only call that touches live ad platforms. The exhaustive route list lives in /api/v1/openapi.json.

The contract

The parts you’d check before adopting.

One envelope, typed errors, honest rate-limit headers, boring pagination, and a written versioning policy.

Response envelope— every endpoint

// success
{ "data": …, "meta": { "nextCursor": "1700000000", "total": 25 } }

// error — always this shape
{ "error": { "code": "INSUFFICIENT_SCOPE",
    "message": "Missing required scope: campaigns:write",
    "details": { "required": ["campaigns:write"] } } }

Error codes— all of them

HTTPCodeWhen
400VALIDATION_ERRORInvalid request body
401INVALID_CREDENTIALSBad, revoked, or expired token
402SUBSCRIPTION_REQUIRED · SUBSCRIPTION_EXPIRED · API_ACCESS_NOT_IN_PLANBilling — resolve, then retry
403INSUFFICIENT_SCOPEToken lacks the required scope
404NOT_FOUNDMissing, or outside the resolved Space
429RATE_LIMITEDMinute or daily limit exceeded
500INTERNAL_ERROROur fault — safe to retry

Rate limits— on every response

HeaderMeaning
X-RateLimit-LimitMinute bucket for this token (default 60)
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetUnix time the window resets
Retry-AfterSeconds to wait, sent with 429

Minute buckets are per-token, so one noisy script can’t starve another. The daily bucket comes from your plan and is shared across your tokens.

Versioning & Spaces— written down

Everything lives under /api/v1. Additions — new fields, endpoints, enum values — ship without notice and are never breaking. Removals and renames go to /api/v2, and old versions run for at least 12 months after a successor is announced.

Send the X-Space-Id header to address one workspace; omit it for your Personal Space. Ids outside the resolved Space return 404 — data never leaks across Spaces.

Auth & scopes · for builders

Tokens built to be scanned, scoped, and rotated.

grow_live_A1B2C3D4E5F6G7H8_veryLongUrlSafeBase64Secret
grow_ — fixed prefix — GitHub secret scanning and trufflehog recognize a leaked token on sight
live | test — environment, visible at a glance in logs
A1B2… — public token ID — safe to log and reference
secret — confidential — only its SHA-256 is stored server-side
  • ·Accepted only in the Authorization header — query-string and cookie forms are rejected
  • ·Up to 5 tokens per account, expiry up to one year, one-click revoke
  • ·Rotation issues a new secret but keeps the token ID — your references survive
  • ·Scopes are immutable — changing access means minting a new token

Scoped access — 13 grants

campaigns:readread
campaigns:writewrite
ads:readread
ads:writewrite
keywords:readread
keywords:writewrite
assets:readread
assets:writewrite
businesses:readread
businesses:writewrite
reports:readread
sync:readread
sync:writewrite

sync:write is deliberately separate from the resource scopes — triggering a sync mutates external ad platforms and can spend real budget. A reporting token never needs it.

Mint a token. Point curl at /me.

Token minting lives in Settings → Developer. Prefer a typed client? The SDKs are generated from this API’s spec.