API reference

Programmatic access to your projects, articles, keywords, and rankings. Bearer-token auth, JSON envelopes, plan quota meters, and rate-limit guidance. Same shape your dashboard uses, exposed to your code.

#Quick start

Base URL

https://growganic.io/api/v1

Every request requires a Bearer API key in the Authorization header. Keys are generated in your dashboard under Settings → API Keys on every plan. The raw key is shown once on creation and stored as a SHA-256 hash; if you lose it, revoke and re-issue.

curl https://growganic.io/api/v1/projects \
  -H "Authorization: Bearer gro_xxxxxxxxxxxxxxxxxxxx"

Rate limits

  • 100 requests per minute per API key.
  • Pipeline runs, keyword tracking, and a few other actions count against your plan's quota (a lifetime credit on Free).
  • Call GET /usage anytime to see what you have left.
  • Upgrading mid-cycle resets your monthly counters, so a 429 from the prior tier clears on your next call.

#Response shape

All responses return JSON. Success payloads use a data envelope. Errors use an error envelope with a machine-readable code and a human-readable message.

Success

{
  "data": { }
}

Error

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or revoked API key."
  }
}

#Error codes

CodeHTTPMeaning
UNAUTHORIZED401Missing, invalid, or revoked API key.
FORBIDDEN403Key is valid but lacks permission for this resource.
NOT_FOUND404Resource does not exist or belongs to another tenant.
VALIDATION_ERROR400Request failed validation.
CONFLICT409Would create a duplicate. The response carries the existing record id.
PRECONDITION_FAILED412Account state blocks the action (for example, domain not verified).
RATE_LIMIT_EXCEEDED429This API key passed 100 requests in a minute. Retry after the Retry-After header.
QUOTA_EXHAUSTED429Your plan's allowance for this action is used up. Retrying will not help until the allowance resets or the plan changes; GET /usage shows each meter and its resetsAt.
TOO_MANY_REQUESTS429This action is cooling down or reached its daily cap. Retry later.
INTERNAL_ERROR500Server error. Retry with backoff.
SERVICE_UNAVAILABLE503Temporarily out of capacity. Retry after a short wait.

#Projects

GET/projects

List every project on your account. Paginated.

Query params

ParamTypeDescription
statusstringOptional. One of active, paused, archived.
limitintDefault 50, max 100.
offsetintDefault 0.

Example request

curl https://growganic.io/api/v1/projects \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "items": [
      {
        "id": "3b1b...",
        "name": "Acme Blog",
        "domain": "acme.com",
        "status": "active",
        "createdAt": "2026-04-01T10:00:00.000Z",
        "updatedAt": "2026-04-07T18:22:10.000Z"
      }
    ],
    "total": 4,
    "limit": 50,
    "offset": 0
  }
}
GET/projects/:id

Fetch a single project.

Example request

curl https://growganic.io/api/v1/projects/3b1b... \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "id": "3b1b...",
    "name": "Acme Blog",
    "domain": "acme.com",
    "status": "active",
    "createdAt": "2026-04-01T10:00:00.000Z",
    "updatedAt": "2026-04-07T18:22:10.000Z"
  }
}

Returns 404 if the project does not belong to your tenant.

#Articles

GET/articles

List articles. Supports filters and pagination.

Query params

ParamTypeDescription
projectIduuidOptional. Filter by project.
statusstringOptional. One of draft, optimizing, review, approved, published, refreshing, removed. A removed article is one deleted from your own CMS; its row and history stay.
limitintDefault 50, max 100.
offsetintDefault 0.

Example request

curl "https://growganic.io/api/v1/articles?projectId=3b1b...&limit=20" \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "items": [
      {
        "id": "a1c2...",
        "projectId": "3b1b...",
        "runId": "7f2e...",
        "title": "Best form builder for small business",
        "slug": "best-form-builder",
        "targetKeyword": "best form builder",
        "status": "published",
        "wordCount": 3102,
        "readingTimeMinutes": 14,
        "overallScore": 89,
        "seoScore": 91,
        "geoScore": 92,
        "readabilityScore": 72,
        "featuredImageUrl": "https://cdn.growganic.io/articles/3b1b.../a1c2.../1.webp",
        "cmsProvider": "wordpress",
        "cmsExternalId": "482",
        "publishedUrl": "https://acme.com/blog/best-form-builder",
        "publishedAt": "2026-04-07T19:10:00.000Z",
        "createdAt": "2026-04-07T18:40:00.000Z",
        "updatedAt": "2026-04-07T19:10:00.000Z"
      }
    ],
    "total": 42,
    "limit": 20,
    "offset": 0
  }
}

wordCount is null when no word count was measured for the article, and readingTimeMinutes (derived from wordCount at 238 words a minute) is null with it. cmsProvider names where the article was delivered: hosted-blog, wordpress, shopify, hubspot or webhook today; an older article can carry the name of a destination since retired, and an article never delivered carries null, so treat it as an open string.

GET/articles/:id

Fetch a single article with full markdown and sanitized body HTML.

Example request

curl https://growganic.io/api/v1/articles/a1c2... \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "id": "a1c2...",
    "projectId": "3b1b...",
    "runId": "7f2e...",
    "title": "Best form builder for small business",
    "slug": "best-form-builder",
    "targetKeyword": "best form builder",
    "status": "published",
    "wordCount": 3102,
    "readingTimeMinutes": 14,
    "content": "Best form builder...",
    "contentHtml": "<p>Best form builder...</p>",
    "excerpt": "Honest, tested guide to the form builders...",
    "metaTitle": "Best Form Builder for Small Business (2026)",
    "metaDescription": "Honest, tested guide to the...",
    "overallScore": 89,
    "seoScore": 91,
    "geoScore": 92,
    "readabilityScore": 72,
    "featuredImageUrl": "https://cdn.growganic.io/articles/3b1b.../a1c2.../1.webp",
    "cmsProvider": "wordpress",
    "cmsExternalId": "482",
    "publishedUrl": "https://acme.com/blog/best-form-builder",
    "publishedAt": "2026-04-07T19:10:00.000Z",
    "createdAt": "2026-04-07T18:40:00.000Z",
    "updatedAt": "2026-04-07T19:10:00.000Z"
  }
}

Includes runId, scores, meta tags, and CMS state when published. Articles are kept on GrowGanic and not deletable through the API. To take a live article down, manage it in your CMS. wordCount is null when no word count was measured for the article, and readingTimeMinutes (derived from wordCount at 238 words a minute) is null with it. cmsProvider names where the article was delivered: hosted-blog, wordpress, shopify, hubspot or webhook today; an older article can carry the name of a destination since retired, and an article never delivered carries null, so treat it as an open string.

POST/articles

Queue a new article. The keyword you pass is the keyword the article targets, with no system reinterpretation. Consumes one article credit from your plan's quota (lifetime on Free).

Request body

{
  "projectId": "3b1b...",
  "keyword": "best form builder for small business"
}

Example request

curl -X POST https://growganic.io/api/v1/articles \
  -H "Authorization: Bearer gro_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "3b1b...",
    "keyword": "best form builder for small business"
  }'

Example response

{
  "data": {
    "runId": "7f2e...",
    "status": "queued"
  }
}

Generation runs in the background, typically a few minutes. Poll GET /runs/:id until articleId is non-null, then fetch GET /articles/:articleId. Articles auto-publish: by default to a GrowGanic-hosted blog (the response will include cmsProvider='hosted-blog' and a https://growganic.io/p/<slug>/<article-slug> publishedUrl), to your external CMS when one is connected (cmsProvider='wordpress' | 'shopify' | 'hubspot' | 'webhook'; older articles can name a destination since retired, so treat cmsProvider as an open string), or to your own subdomain/path-rewrite when configured in /sites/publishing. Calling with a keyword that already has an article in the project returns 409 with the existing article id.

#Runs

GET/runs

List article-generation runs you started through the API.

Query params

ParamTypeDescription
projectIduuidOptional. Filter by project.
statusstringOptional. One of pending, running, completed, failed, cancelled.
limitintDefault 50, max 100.
offsetintDefault 0.

Example request

curl "https://growganic.io/api/v1/runs?projectId=3b1b...&status=completed&limit=20" \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "items": [
      {
        "id": "7f2e...",
        "projectId": "3b1b...",
        "status": "completed",
        "startedAt": "2026-04-27T18:40:02.000Z",
        "completedAt": "2026-04-27T18:46:11.000Z",
        "durationMs": 369000,
        "error": null,
        "articleId": "a1c2...",
        "createdAt": "2026-04-27T18:40:00.000Z"
      }
    ],
    "total": 17,
    "limit": 20,
    "offset": 0
  }
}

articleId fills in mid-run as soon as the article exists. Final scores arrive when status is completed.

GET/runs/:id

Poll a single run by id. Cheaper than listing.

Example request

curl https://growganic.io/api/v1/runs/7f2e... \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "id": "7f2e...",
    "projectId": "3b1b...",
    "status": "running",
    "progress": 62,
    "startedAt": "2026-04-27T18:40:02.000Z",
    "completedAt": null,
    "durationMs": null,
    "error": null,
    "articleId": "a1c2...",
    "createdAt": "2026-04-27T18:40:00.000Z"
  }
}

progress is 0..100. status reaches completed when scores are final. Once articleId is set you can fetch the article. error is null unless the run failed, and then one short sentence from a fixed set (for example: Article generation failed.), never internal error text.

#Keywords

GET/keywords

List tracked keywords for a project. These are the keywords that appear on the Rankings page and receive daily position checks.

Query params

ParamTypeDescription
projectIdrequireduuidProject to query.
limitintDefault 50, max 200.
offsetintDefault 0.

Example request

curl "https://growganic.io/api/v1/keywords?projectId=3b1b..." \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "items": [
      {
        "id": "c4f1e2a8-5b3d-4e6f-9a7b-2d8c1e0f4a6b",
        "keyword": "best form builder",
        "countryCode": "us",
        "country": "United States",
        "device": "desktop",
        "currentPosition": 4,
        "previousPosition": 6,
        "bestPosition": 3,
        "lastCheckedAt": "2026-04-11T03:00:00.000Z",
        "createdAt": "2026-03-20T12:00:00.000Z"
      }
    ],
    "total": 18,
    "limit": 50,
    "offset": 0
  }
}
POST/keywords/track

Add new keywords to rank tracking. Up to 100 per request. Keywords are trimmed; duplicates and keywords already tracked are ignored and do not count against your cap. A keyword you untracked earlier comes back with its history.

Request body

{
  "projectId": "3b1b...",
  "keywords": [
    "best form builder",
    "free survey tool",
    "typeform alternative"
  ]
}

Example request

curl -X POST https://growganic.io/api/v1/keywords/track \
  -H "Authorization: Bearer gro_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "3b1b...",
    "keywords": ["best form builder", "free survey tool", "typeform alternative"]
  }'

Example response

{
  "data": {
    "tracked": 3
  }
}

Capped at your plan's tracked-keyword limit per project (Free 50, Pro 200, Business 500). Returns 403 FORBIDDEN if the request would exceed the cap. tracked counts the keywords this call started tracking; a keyword already tracked is ignored and not counted.

DELETE/keywords/:id

Untrack a keyword. The id is the keyword's id (a UUID) as returned by GET /keywords.

Example request

curl -X DELETE https://growganic.io/api/v1/keywords/c4f1e2a8-5b3d-4e6f-9a7b-2d8c1e0f4a6b \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "deleted": true,
    "id": "c4f1e2a8-5b3d-4e6f-9a7b-2d8c1e0f4a6b"
  }
}

Soft-delete. Rank-history rows are preserved for analytics; the keyword just stops accruing new daily checks and disappears from GET /keywords. Re-track via POST /keywords/track. Returns 404 if the id does not belong to your tenant.

#Rankings

GET/rankings

Historical rank check data for a project.

Query params

ParamTypeDescription
projectIdrequireduuidProject to query.
keywordstringOptional. Filter to a single keyword.
daysintDefault 30, max 365. Lookback window.
limitintDefault 200, max 500.
offsetintDefault 0. Rows to skip; page with offset += limit until offset >= total.

Example request

curl "https://growganic.io/api/v1/rankings?projectId=3b1b...&days=7" \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "items": [
      {
        "keyword": "best form builder",
        "position": 4,
        "gscPosition": 3,
        "url": "https://acme.com/blog/best-form-builder",
        "featuredSnippet": false,
        "aiOverviewCited": true,
        "source": "serper",
        "countryCode": "us",
        "country": "United States",
        "device": "desktop",
        "checkedAt": "2026-04-11T03:00:00.000Z"
      }
    ],
    "total": 412,
    "limit": 200,
    "offset": 0,
    "days": 7
  }
}

Rows are the raw daily checks, newest first, and total counts every row in the window. position is that check's own read: null means the site was not found in the depth it scanned (the held value is currentPosition on GET /keywords). gscPosition is Google Search Console's averaged position over the day, null when GSC is not connected or the keyword had no impressions that day. featuredSnippet is null when no page of results was read for the check (source gsc). aiOverviewCited is the standing AI Overview measurement for the keyword, null when it was not measured or no citation was read yet.

#Usage

GET/usage

Plan quota meters and active plan. Use this to budget API article generation, surface 'you have N articles left' in your own UI, or back off when you hit a 429.

Example request

curl https://growganic.io/api/v1/usage \
  -H "Authorization: Bearer gro_..."

Example response

{
  "data": {
    "plan": {
      "id": "free",
      "name": "Free",
      "status": null
    },
    "meters": [
      { "label": "Articles written", "used": 1, "total": 1, "resetsAt": null },
      { "label": "keyword searches", "used": 0, "total": 20, "resetsAt": null },
      { "label": "competitor scans", "used": 0, "total": 1, "resetsAt": null },
      { "label": "reports", "used": 0, "total": 1, "resetsAt": null }
    ],
    "daysUntilReset": null
  }
}

meters are the usage counters the dashboard shows and the rate limiter enforces. Each has its own resetsAt, null when nothing is counted yet and on the free plan, whose article credit is a lifetime one. daysUntilReset is deprecated and null when there is no reset to count down to. plan.status is null on the free plan. Tracked-keyword caps are per-project (see GET /keywords) and are not part of the meters.

#Webhooks

When an article publishes, GrowGanic POSTs a signed JSON payload to a URL you control. Use it to push articles into a custom CMS, a static-site generator, or an automation tool (Zapier, Make, n8n).

The full payload schema, header reference, signature verification example, and retry policy live on the dedicated webhook integration page:

/docs/webhooks has the full reference.

Usage tips

  • Keys are tenant-scoped. Every API call is logged to your usage feed for audit.
  • Never commit keys to source control. Rotate by revoking and re-issuing.
  • Use projectId filters aggressively to keep payloads small.
  • Content generation and rank checks are async. Poll the list endpoint to detect completion.
  • If a request fails with 429, read the message field for the reset window.