Documentation
API reference
Programmatic access to your projects, articles, keywords, and rankings. Bearer-token auth, JSON envelopes, monthly quota meters, and rate-limit guidance. Same shape your dashboard uses, exposed to your code.
01
#Quick start
Base URL
https://growganic.io/api/v1Every request requires a Bearer API key in the Authorization header. Keys are generated in your dashboard under Settings → API Keys (Business 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 monthly quota.
- 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.
02
#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."
}
}03
#Error codes
| Code | HTTP | Meaning |
|---|---|---|
| UNAUTHORIZED | 401 | Missing, invalid, or revoked API key. |
| FORBIDDEN | 403 | Key is valid but lacks permission for this resource. |
| NOT_FOUND | 404 | Resource does not exist or belongs to another tenant. |
| VALIDATION_ERROR | 400 | Request failed validation. |
| CONFLICT | 409 | Would create a duplicate. The response carries the existing record id. |
| PRECONDITION_FAILED | 412 | Account state blocks the action (for example, domain not verified). |
| RATE_LIMIT_EXCEEDED | 429 | Per-minute or monthly quota exhausted. |
| TOO_MANY_REQUESTS | 429 | A recent run is still settling. Retry shortly. |
| INTERNAL_ERROR | 500 | Server error. Retry with backoff. |
04
#Projects
/projectsList every project on your account. Paginated.
Query params
| Param | Type | Description |
|---|---|---|
| status | string | Optional. One of active, paused, archived. |
| limit | int | Default 50, max 100. |
| offset | int | Default 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
}
}/projects/:idFetch 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.
05
#Articles
/articlesList articles. Supports filters and pagination.
Query params
| Param | Type | Description |
|---|---|---|
| projectId | uuid | Optional. Filter by project. |
| status | string | Optional. One of draft, optimizing, review, approved, published, refreshing. |
| limit | int | Default 50, max 100. |
| offset | int | Default 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://images.example.com/hero/a1c2.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
}
}/articles/:idFetch 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://images.example.com/hero/a1c2.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, schema markup, 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.
/articlesQueue 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 monthly quota.
Request body
{
"projectId": "3b1b...",
"keyword": "best form builder for small business",
"targetWordCount": 3000
}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",
"targetWordCount": 3000
}'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. No CMS required: articles are fetchable here. If a CMS is connected, the article also publishes there. Calling with a keyword that already has an article in the project returns 409 with the existing article id.
06
#Runs
/runsList article-generation runs you started through the API.
Query params
| Param | Type | Description |
|---|---|---|
| projectId | uuid | Optional. Filter by project. |
| status | string | Optional. One of pending, running, completed, failed, cancelled. |
| limit | int | Default 50, max 100. |
| offset | int | Default 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.
/runs/:idPoll 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.
07
#Keywords
/keywordsList tracked keywords for a project. These are the keywords that appear on the Rankings page and receive daily position checks.
Query params
| Param | Type | Description |
|---|---|---|
| projectIdrequired | uuid | Project to query. |
| limit | int | Default 50, max 200. |
| offset | int | Default 0. |
Example request
curl "https://growganic.io/api/v1/keywords?projectId=3b1b..." \
-H "Authorization: Bearer gro_..."Example response
{
"data": {
"items": [
{
"id": "kw_1",
"keyword": "best form builder",
"location": "US",
"device": "desktop",
"currentPosition": 4,
"previousPosition": 6,
"bestPosition": 3,
"lastCheckedAt": "2026-04-11T03:00:00.000Z",
"createdAt": "2026-03-20T12:00:00.000Z"
}
],
"limit": 50,
"offset": 0
}
}/keywords/trackAdd new keywords to rank tracking. Up to 100 per request. Duplicates are silently ignored.
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"]
}'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.
/keywords/:idUntrack a keyword. The id is the kw_... value returned by GET /keywords.
Example request
curl -X DELETE https://growganic.io/api/v1/keywords/kw_1 \
-H "Authorization: Bearer gro_..."Example response
{
"data": {
"deleted": true,
"id": "kw_1"
}
}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.
08
#Rankings
/rankingsHistorical rank check data for a project.
Query params
| Param | Type | Description |
|---|---|---|
| projectIdrequired | uuid | Project to query. |
| keyword | string | Optional. Filter to a single keyword. |
| days | int | Default 30, max 365. Lookback window. |
| limit | int | Default 200, max 500. |
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.2,
"url": "https://acme.com/blog/best-form-builder",
"featuredSnippet": false,
"aiOverviewCited": true,
"location": "US",
"device": "desktop",
"checkedAt": "2026-04-11T03:00:00.000Z"
}
],
"days": 7,
"limit": 200
}
}Each row reports both the organic Google position and gscPosition (Google Search Console's averaged position over the day, when GSC is connected). gscPosition is null when GSC is not connected for the project, or when the keyword had no impressions in GSC for that day.
09
#Usage
/usageCurrent month's 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": "Published articles", "used": 1, "total": 3 },
{ "label": "keyword searches", "used": 0, "total": 20 },
{ "label": "competitor scans", "used": 0, "total": 1 },
{ "label": "reports", "used": 0, "total": 1 }
],
"daysUntilReset": 4
}
}meters return the same counters the rate limiter enforces, so the numbers stay consistent. Lifetime plans return status: 'lifetime'. Tracked-keyword caps are per-project (see GET /keywords) and are intentionally not part of the monthly meters.
10
#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.