Developers
The gx360 API and webhooks.
Pull your growth data into your own dashboards and tools, and get told the moment work moves, a lead arrives or an audit finishes. Read-only REST, signed webhooks, plain JSON.
Getting started
Three steps to your first call.
- The API and webhooks are part of the Scale plan and Done for you.
- A company admin opens Settings → Developers in gx360 and creates an API key. The key is shown once: store it like a password.
- Call the API with the key. Every call is read-only, and a key only ever sees its own company’s data.
curl https://app.gx360.ai/api/public/v1/work-items?status=open&per_page=20 \
-H "Authorization: Bearer gx_live_xxxxxxxxxx_…"Authentication
One bearer key per integration.
Send the key in the Authorization header: Bearer gx_live_…. Keys never go in the URL. Make a separate key for each tool, named after it, so you can revoke one without breaking the others. Settings shows when each key was last used.
Base URL: https://app.gx360.ai/api/public/v1
Requests and pages
JSON in a data envelope.
Lists are paged: pass page and per_page (up to 100, default 50). The meta object says whether there is more. Dates are ISO 8601 in UTC; filters like updated_since take a date or a date-time. To sync, keep the newest updated_at you saw and pass it next time.
{
"data": [
{
"id": 4182,
"site_id": 3,
"title": "Pricing page title is cut off in Google",
"category": "seo",
"priority": "P1",
"status": "open",
"page": "/pricing",
"keyword": "time tracking software pricing",
"summary": "The title is 74 characters…",
"expected_impact": 0.6,
"created_at": "2026-10-07T06:12:40+00:00",
"updated_at": "2026-10-07T06:12:40+00:00"
}
],
"meta": { "page": 1, "per_page": 20, "total": 37, "has_more": true }
}Errors and limits
Plain status codes, one error shape.
Each key may make 120 requests a minute. Errors always look like this:
{
"error": {
"code": "rate_limited",
"message": "Too many requests: 120 a minute per key."
}
}| Status | code | Meaning |
|---|---|---|
| 401 | unauthenticated | The key is missing, wrong or revoked. |
| 403 | plan_required | The company’s plan does not include the API. |
| 404 | not_found | No such record in your company (another company’s site_id is also 404). |
| 422 | invalid_request | A parameter is wrong; fields says which. |
| 429 | rate_limited | More than 120 requests a minute on this key. Wait for Retry-After seconds. |
Endpoints
What you can read.
| Endpoint | Returns | Parameters |
|---|---|---|
GET /sites | Your websites. | — |
GET /work-items | Problems and proposals on the Work board, newest change first. | site_id, status, updated_since, page, per_page |
GET /kra-plans | KRA plans with each KRA, its KPIs, targets and current values. | site_id |
GET /leads | Leads from your connected CRM. | site_id, status, created_since, page, per_page |
GET /search/summary | Google Search clicks, impressions, CTR and position against the previous period. | site_id (required), days = 7, 28 or 90 |
GET /audit/summary | The latest site audit and open problems by severity. | site_id (required) |
Work item status is one of open, accepted, in_progress, done, dismissed or superseded. Need something that isn’t here? Tell us; the API grows with what customers use.
Webhooks
Get told, instead of asking.
In Settings → Developers, add an https endpoint and pick its events. You get a signing secret once; use it to check each delivery. Send test posts a ping event so you can check your receiver straight away.
| Event | Sent when |
|---|---|
work_item.created | A new problem or proposal is on the Work board. |
work_item.status_changed | A work item moved: accepted, in progress, done, dismissed… Includes previous_status. |
kra_plan.scored | A KRA plan was scored again, with its new overall score. |
lead.created | A new lead arrived from your CRM. |
audit.completed | A site audit finished. |
The data object uses the same fields as the API. The body’s id equals the X-Gx360-Delivery header and stays the same across retries, so you can ignore one you’ve already handled.
POST /hooks/gx360 HTTP/1.1
Content-Type: application/json
User-Agent: gx360-webhooks/1
X-Gx360-Event: work_item.status_changed
X-Gx360-Delivery: 9b0d6f3e-2c8a-4f8e-9a51-3c1f7d2b6e10
X-Gx360-Signature: t=1791360000,v1=5f2b…c9
{
"id": "9b0d6f3e-2c8a-4f8e-9a51-3c1f7d2b6e10",
"event": "work_item.status_changed",
"created_at": "2026-10-07T09:20:00+00:00",
"data": {
"id": 4182,
"site_id": 3,
"title": "Pricing page title is cut off in Google",
"status": "done",
"previous_status": "in_progress"
}
}Verifying signatures
Check every delivery came from gx360.
X-Gx360-Signature holds a Unix time t and v1, the hex HMAC-SHA256 of "<t>.<raw body>" with your endpoint’s secret. Compute it over the raw body before parsing the JSON, compare in constant time, and reject anything older than five minutes.
import crypto from 'node:crypto'
// rawBody: the request body exactly as received (a Buffer or string).
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const age = Math.abs(Date.now() / 1000 - Number(parts.t))
if (age > 300) return false // older than five minutes: reject
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`).digest('hex')
const given = Buffer.from(parts.v1 ?? '')
return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), given)
}<?php
function gx360_verify(string $rawBody, string $header, string $secret): bool
{
parse_str(str_replace(',', '&', $header), $parts);
if (abs(time() - (int) ($parts['t'] ?? 0)) > 300) {
return false; // older than five minutes: reject
}
$expected = hash_hmac('sha256', $parts['t'].'.'.$rawBody, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}Retries
Answer 2xx within ten seconds.
Any other answer, a timeout or a redirect counts as a failure. We retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then mark the delivery failed. The delivery log in Settings shows each attempt and what your server answered. After 50 failures in a row the endpoint is paused; switch it back on when it’s fixed. Do slow work after you’ve answered, not before.