Account API
403 until you upgrade.Account keys vs. site keys vs. group keys
A site key (goal_live_...) is scoped to one WordPress install: it's
what the plugin uses to send events, and what the read API
and MCP use to read them back for that one site.
An account key (goal_acct_...) is scoped to your whole account. It can
create, rename, reactivate and delete sites, mint and revoke their site
keys, manage groups and their group keys,
and read events across every site you own in one call. There's no
X-Site-Domain header to pass — the key itself carries the account scope.
A group key (goal_grp_...) sits in between: it's scoped to exactly one
group, everything under that group and nothing else. See
Group-scoped keys below.
Use a site key for the WordPress plugin and single-site integrations. Use an account key for anything that manages sites, or that needs a cross-account view — dashboards, CI, an AI tool connected over MCP. Use a group key when you want to hand a customer or sub-team API access to their sites only, without giving them the run of your whole account.
Creating an account key
Go to app.hellolog.io → Settings → API keys and click Create key. The key is shown exactly once — copy it before leaving the page, helloLOG only stores its hash. You can have up to 10 active account keys at a time, and revoke any of them from the same screen; a revoked key stops working within seconds.
Group keys work differently: there's no dashboard
button for them today — mint one by calling POST /groups/{id}/keys with an existing unscoped account
key.
Authentication
Every request needs the account key as a bearer token:
curl https://api.hellolog.io/v1/wordpress-activity-audit-log/account/sites \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
All endpoints below are relative to:
https://api.hellolog.io/v1/wordpress-activity-audit-log/account
Account info
Get account info
GET /me returns your account's identity and current quota usage — email,
plan, reseller status, effective limits, and a snapshot of how many sites,
paused sites, and groups you have. Useful for confirming which account a
key belongs to, or checking quota headroom before a call would otherwise
fail with a 409.
curl https://api.hellolog.io/v1/wordpress-activity-audit-log/account/me \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
{
"account_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "[email protected]",
"plan": "starter",
"is_reseller": false,
"limits": { "sites": 10, "retention_days": 90, "keys_per_site": 2 },
"usage": { "sites_active": 4, "sites_paused": 0, "groups": 2 },
"key": { "prefix": "4a1b2c3d", "created_at": "2026-08-01T00:00:00Z" }
}
key describes the account key making this request, not every key on
the account — its prefix is what you'd look for on app.hellolog.io →
Settings → API keys to revoke it.
On a reseller account, limits reports
-1 for every field instead of a number, and is_reseller is true —
plan still shows the account's real plan slug, unaffected either way.
Calling GET /me with a group key instead of an
unscoped account key adds a group field and narrows usage to that
group alone:
{
"account_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "[email protected]",
"plan": "starter",
"is_reseller": false,
"limits": { "sites": 10, "retention_days": 90, "keys_per_site": 2 },
"usage": { "sites_active": 1, "sites_paused": 0, "groups": 1 },
"key": { "prefix": "ab12cd34", "created_at": "2026-08-20T09:00:00Z" },
"group": { "id": "b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Agency clients — Acme" }
}
group is present only for a group key — its mere presence tells you
which kind of key you're holding without inspecting anything else.
usage.sites_active / sites_paused count only that group's sites, and
usage.groups is always 1 (its own group — a group key has no
visibility into any sibling group, even a count of them). An unscoped
account key never gets a group field, and its usage always covers the
whole account.
Sites
List sites
GET /sites returns every site on your account. A group key
sees only the sites in its own group — nothing else on the account.
curl https://api.hellolog.io/v1/wordpress-activity-audit-log/account/sites \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
{
"sites": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"domain": "shop.example.com",
"site_url": "https://shop.example.com",
"name": "Main store",
"status": "active",
"retention_days": 90,
"created_at": "2026-07-01T10:00:00Z",
"active_token_count": 1,
"group_id": "b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"group_name": "Agency clients — Acme"
}
]
}
name is nullable — sites created without one come back as null. So are
group_id and group_name: they're null together for a site that isn't
in a group, and always non-null together for one that is.
Add a site
POST /sites registers a new site and mints its first site key in the same
call. domain is the bare hostname; site_url is the full http(s) URL.
name is optional, and so is group_id — assign the new site straight into
an existing group at creation time, instead of a separate
PATCH /sites/{id} call right after.
curl -X POST https://api.hellolog.io/v1/wordpress-activity-audit-log/account/sites \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..." \
-H "Content-Type: application/json" \
-d '{"domain":"shop.example.com","site_url":"https://shop.example.com","name":"Main store","group_id":"b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"}'
{
"site_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"domain": "shop.example.com",
"token": "goal_live_ab12cd34_...",
"prefix": "ab12cd34"
}
token is a fresh site key, shown once — paste it straight into the
plugin's Tools → helloLOG → Settings screen. The response never echoes
group_id/group_name, even when you sent one — check GET /sites right after if you need to confirm the assignment.
The group assignment is atomic with the create itself: a group_id that
doesn't parse as a UUID, or names a group that isn't yours, fails the whole
call with 404 not found and persists nothing — not even the site, so
there's no orphaned ungrouped site left behind to clean up afterward. Other
errors: 400 invalid domain, 400 site_url must be an http(s) URL, 409 site already registered, 409 plan site limit reached.
A group key can create sites too, always into its
own group: omit group_id and the new site is auto-assigned to the key's
group; send it explicitly and it must match the key's own group exactly
— any other value (a sibling group, or no group at all) answers 404,
same as an unscoped key naming a group it doesn't own.
Update a site
PATCH /sites/{id} updates a site's display name, its group, or both —
send at least one of name / group_id.
name— sending an empty string clears it.group_id— omit the field to leave the site's group alone; sendnullto take it out of its group; send a group's UUID to move it in. The group must be one of your own — see Site groups below.
curl -X PATCH https://api.hellolog.io/v1/wordpress-activity-audit-log/account/sites/3fa85f64-5717-4562-b3fc-2c963f66afa6 \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..." \
-H "Content-Type: application/json" \
-d '{"name":"Main store (EU)","group_id":"b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"}'
204 No Content on success. 400 name or group_id required if the body
has neither field. 404 not found if group_id doesn't parse as a UUID
or names a group that isn't yours (same anti-enumeration rule as a
foreign site id).
A group key can only reach this endpoint for a site
already in its own group (any other site 404s before the body is even
read). Sending group_id at all from a group key — even reasserting the
site's own current group, or null to try to ungroup it — answers 404
and changes nothing. A group key can rename its own sites; only an
unscoped account key can move a site between groups.
Reactivate a site
POST /sites/{id}/activate re-activates a paused site, as long as your plan
still has a free site slot. Calling it on an already-active site is a no-op.
curl -X POST https://api.hellolog.io/v1/wordpress-activity-audit-log/account/sites/3fa85f64-5717-4562-b3fc-2c963f66afa6/activate \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
204 No Content on success. 409 plan site limit reached if you're at your
plan's site cap. 409 site cannot be reactivated — contact support for a
revoked site — revoked (as opposed to paused) sites can only be brought
back by helloLOG support.
Delete a site
DELETE /sites/{id} removes the site, every site key
issued for it, and all of its events. There's no undo and no recovery
window.The endpoint refuses to run without an explicit confirm=true query
parameter:
curl -X DELETE "https://api.hellolog.io/v1/wordpress-activity-audit-log/account/sites/3fa85f64-5717-4562-b3fc-2c963f66afa6?confirm=true" \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
204 No Content on success. Without confirm=true, 400:
{
"error": "destructive: pass ?confirm=true — the site, its keys, and its events are removed"
}
404 on all of
them, identical to a site on a different account entirely.Site groups
Groups organize your sites like folders — an agency running multiple client sites might have one group per client. A site belongs to at most one group; there's no nesting, and groups carry no plan limit — every plan, including Free, can create as many as it wants. Deleting a group ungroups its sites, it never deletes them, and none of their events are touched.
Each group also carries an optional external_ref — your own identifier
for it (a CRM client id, for example), unique per account. Set it on
create or update it later, then look the group back up
by that value with GET /groups?external_ref= instead of
tracking helloLOG's own id.
In the dashboard, groups are managed from the Sites page; the
Logs page can filter events by group the same way ?group_id=
does below.
List groups
GET /groups returns every group on your account, each with its
current site count. A group key gets back exactly
one row — its own group — never a sibling's.
curl https://api.hellolog.io/v1/wordpress-activity-audit-log/account/groups \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
{
"groups": [
{
"id": "b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Agency clients — Acme",
"external_ref": "crm-4471",
"site_count": 3,
"created_at": "2026-08-20T09:00:00Z"
}
]
}
external_ref is null for a group that doesn't have one set — see
Create a group below.
Filter by external_ref
Pass ?external_ref=<value> for an exact match against your own groups —
useful once your own system tracks a group by that value instead of
helloLOG's id.
curl "https://api.hellolog.io/v1/wordpress-activity-audit-log/account/groups?external_ref=crm-4471" \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
No match still answers 200 with an empty groups array — it's a filter,
not a lookup, so there's no 404 for "no group has that external_ref".
Create a group
POST /groups. name is required; external_ref is optional. Omit it, or
send an empty string, for no external_ref.
curl -X POST https://api.hellolog.io/v1/wordpress-activity-audit-log/account/groups \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..." \
-H "Content-Type: application/json" \
-d '{"name":"Agency clients — Acme","external_ref":"crm-4471"}'
{
"id": "b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Agency clients — Acme",
"external_ref": "crm-4471"
}
201 Created on success. Errors: 400 name required (empty/missing
name), 400 name too long (over 200 characters), 400 external_ref too long (over 200 characters), 409 group name already exists (you already
have a group with that name — group names are unique per account, not
globally), 409 external_ref already exists (same rule, but for
external_ref — a separate uniqueness check, so the two 409s never
collide with each other).
Update a group
PATCH /groups/{id} renames a group, changes its external_ref, or both
— send at least one of the two fields.
name— required non-empty when sent; a group's name has no "clear" state.external_ref— tri-state, the same conventionPATCH /sites/{id}'sgroup_iduses: omit the field to leave it alone, sendnull(or an empty string) to clear it, send a value to set it.
curl -X PATCH https://api.hellolog.io/v1/wordpress-activity-audit-log/account/groups/b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..." \
-H "Content-Type: application/json" \
-d '{"name":"Agency clients — Acme Corp","external_ref":"crm-4471-v2"}'
204 No Content on success. 400 name or external_ref required if the
body has neither field. Otherwise the same errors as create a
group: 400 name required / 400 name too long when
name is sent, 400 external_ref too long, 409 group name already exists, 409 external_ref already exists.
Delete a group
DELETE /groups/{id} removes the group. Every site in it has its
group_id set back to null by the deletion itself — the sites, and
all of their events, are left exactly as they were.
curl -X DELETE https://api.hellolog.io/v1/wordpress-activity-audit-log/account/groups/b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
204 No Content on success.
Group-scoped keys
A group key looks like goal_grp_... and authenticates the same way an
account key does — as a bearer token, against the same
https://api.hellolog.io/v1/wordpress-activity-audit-log/account base
URL. Once minted, it behaves like a narrower account key: it can list,
create (auto-assigned into its own group), rename, reactivate, delete and
manage the tokens of sites in its own group only, and its GET /events always carries an implicit group_id filter for that
group. A sibling group's sites, or ungrouped sites, answer 404 — the
same can't-enumerate rule the rest of this API follows, not a 403, so a
group key can't even confirm a foreign site or group exists.
What a group key cannot do, no matter whose group it is — including
its own: create, rename, or delete groups, or mint, list, or revoke group
keys. Those five actions require an unscoped account key
(goal_acct_...); a group key gets 403:
{ "error": "account key required" }
Only an unscoped account key can mint a group key in the first place — there's no self-service path for a group key to mint a sibling or replace itself. Each group has its own 10 active group keys cap, independent of the account's own 10-active-account-keys cap and of every other group's cap.
Deleting a group (see Delete a group above) revokes every key minted for it — immediately, not at the next cache refresh — as part of the same operation. There's no way to "orphan" a group key by deleting its group; it simply stops working.
Mint a group key
POST /groups/{id}/keys — unscoped account key only. name is
optional and defaults to "group key" when omitted.
curl -X POST https://api.hellolog.io/v1/wordpress-activity-audit-log/account/groups/b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d/keys \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..." \
-H "Content-Type: application/json" \
-d '{"name":"Acme portal access"}'
{
"key": "goal_grp_ab12cd34_...",
"prefix": "ab12cd34",
"name": "Acme portal access"
}
201 Created on success. key is the full token, shown exactly
once — copy it now, helloLOG only stores its hash. 404 not found if
{id} isn't a valid UUID or isn't one of your groups. 409 ten active group keys already — revoke one before minting another past this
group's cap.
List a group's keys
GET /groups/{id}/keys — unscoped account key only. Returns every
key ever minted for the group, active and revoked, newest first —
the same audit-trail convention as list a site's keys.
curl https://api.hellolog.io/v1/wordpress-activity-audit-log/account/groups/b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d/keys \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
{
"keys": [
{ "prefix": "ab12cd34", "name": "Acme portal access", "created_at": "2026-08-20T09:00:00Z", "last_used_at": "2026-08-22T14:12:00Z", "revoked_at": null }
]
}
Revoke a group key
DELETE /groups/{id}/keys/{prefix} — unscoped account key only.
curl -X DELETE https://api.hellolog.io/v1/wordpress-activity-audit-log/account/groups/b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d/keys/ab12cd34 \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
204 No Content on success. 404 not found if the prefix doesn't exist,
belongs to a different group (even a group on your own account), or was
already revoked — same anti-enumeration rule as everywhere else on this
API.
| Status | When | Message |
|---|---|---|
| 201 | Group key minted | — |
| 204 | Group key revoked | — |
| 403 | Any group-key endpoint (POST/GET/DELETE .../keys*) called with a goal_grp_ key instead of an unscoped goal_acct_ key — including a key acting on its own group | account key required |
| 404 | {id} isn't a valid UUID or isn't one of your groups | not found |
| 404 | {prefix} doesn't exist, belongs to a different group, or is already revoked | not found |
| 409 | POST .../keys past this group's 10-active-key cap | ten active group keys already — revoke one before minting another |
Group errors
A missing group id and a foreign one — a group that belongs to someone
else's account — both answer the same 404, the same
can't-enumerate-another-tenant's-ids rule the rest of this API follows.
| Status | When | Message |
|---|---|---|
| 400 | POST/PATCH /groups/{id} with an empty or missing name (on PATCH, only when name is sent) | name required |
| 400 | POST/PATCH /groups/{id} with a name over 200 characters | name too long |
| 400 | PATCH /groups/{id} with neither name nor external_ref in the body | name or external_ref required |
| 400 | POST/PATCH /groups/{id} with an external_ref over 200 characters | external_ref too long |
| 404 | Group id doesn't exist, isn't yours, or isn't a valid UUID — on any endpoint that takes a group_id (group CRUD, site creation, site update, the events filter) | not found |
| 409 | POST /groups or PATCH /groups/{id} with a name you already have on another group | group name already exists |
| 409 | POST /groups or PATCH /groups/{id} with an external_ref you already have on another group | external_ref already exists |
Site keys
List a site's keys
GET /sites/{id}/tokens lists every key a site has ever had — active
and revoked — newest first.
curl https://api.hellolog.io/v1/wordpress-activity-audit-log/account/sites/3fa85f64-5717-4562-b3fc-2c963f66afa6/tokens \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
{
"tokens": [
{ "prefix": "ab12cd34", "created_at": "2026-08-01T00:00:00Z", "last_used_at": "2026-08-20T00:00:00Z", "revoked_at": null },
{ "prefix": "ef56gh78", "created_at": "2026-08-10T00:00:00Z", "last_used_at": null, "revoked_at": "2026-08-15T00:00:00Z" }
]
}
revoked_at is null for an active key and set for a revoked one — both
stay in this list indefinitely, so it doubles as an audit trail, not just
a view of what's currently active. prefix is what
revoke a site key takes.
Mint a new site key
POST /sites/{id}/tokens issues an additional site key for an existing
site — useful for rotating without downtime.
curl -X POST https://api.hellolog.io/v1/wordpress-activity-audit-log/account/sites/3fa85f64-5717-4562-b3fc-2c963f66afa6/tokens \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
{
"token": "goal_live_ef56gh78_...",
"prefix": "ef56gh78"
}
A site can only have 2 active keys at a time. 409 two active keys already — revoke one before minting another if you try for a third.
Revoke a site key
DELETE /sites/{id}/tokens/{prefix} revokes one of a site's keys by its
prefix (the short identifier returned alongside the token, not the token
itself).
curl -X DELETE https://api.hellolog.io/v1/wordpress-activity-audit-log/account/sites/3fa85f64-5717-4562-b3fc-2c963f66afa6/tokens/ef56gh78 \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
204 No Content on success.
Events
List events across your account
GET /events returns events from every site on your account, newest first,
cursor-paginated — the same shape as the read API, plus an
optional site_id to narrow it to one site.
A group key has an implicit group_id filter for
its own group applied to every call, even one with no query
parameters at all — you'll never see another group's events through it.
An explicit site_id or group_id naming anything outside the key's own
group answers 404, the same as any other cross-group access on this API.
| Parameter | Description |
|---|---|
site_id | Restrict to one site. Must be a site on your own account. |
group_id | Restrict to every site in one group. Must be a group on your own account. AND-combines with every other filter, including site_id — if the site isn't in that group, the result is simply empty, not an error. |
limit | Page size, 1–200. Defaults to 50. |
from / to | ISO 8601 time bounds on when the event occurred. |
code | Filter by event code. Repeatable or comma-separated. |
user_id | Filter by WordPress user ID. |
object | Filter by the object type the event touched (e.g. post, user). |
event_type | Filter by event type (e.g. created, deleted). |
severity | Filter by severity. Repeatable or comma-separated. Closed set: info, low, medium, high, critical — anything else answers 400 {"error":"invalid severity"}. |
q | Free-text search over the event message. |
cursor | Opaque cursor from a previous response's next_cursor. |
curl "https://api.hellolog.io/v1/wordpress-activity-audit-log/account/events?site_id=3fa85f64-5717-4562-b3fc-2c963f66afa6&code=9000&limit=50" \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
{
"events": [
{
"id": 918273,
"occurred_at": "2026-08-18T09:41:02Z",
"code": 9000,
"severity": "info",
"object": "product",
"event_type": "created",
"user_id": 4,
"username": "a.kovacs",
"message": "WC product \"Merino scarf\" created.",
"site_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"domain": "shop.example.com",
"site_name": "Main store"
}
],
"next_cursor": "eyJ0IjoiMjAyNi0wOC0xOFQwOTo0MDo1OVoiLCJpIjo5MTgyNzB9"
}
Pass next_cursor back as cursor to fetch the next page. An empty
next_cursor means you're on the last page.
site_id tells you which of your sites each row came from — the field
that matters once you're not scoping the call to a single site_id.
domain and site_name are omitempty: they're only absent if the
event's site was deleted after the event was written, which shouldn't
come up in practice since deleting a site cascades its events too.
Filtering by group works the same way, combined with any other filter — here, every critical event across all of one client's sites:
curl "https://api.hellolog.io/v1/wordpress-activity-audit-log/account/events?group_id=b2b1c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d&code=9000&limit=50" \
-H "Authorization: Bearer goal_acct_xxxxxxxx_..."
Same response shape as above — next_cursor paginates it the same way.
Rate limits
Account keys — unscoped or group-scoped — are
rate-limited to 10 requests/second, burst 30, per key. Going over it
returns 429. Each key gets its own independent bucket, so a group key
never shares its budget with the account's unscoped key or with a sibling
group's key, and vice versa.
Reseller accounts are exempt from this limit entirely, on every key they hold, scoped or not.
Errors
Every error response is {"error": "<message>"}. Site-id errors — a
missing site, or a site that belongs to someone else's account — both come
back as a plain 404, so a foreign site id is indistinguishable from one
that doesn't exist. Group ids follow the exact same rule — see
Group errors above for the group-specific table. A
group key reaching outside its own group — a
sibling group's or an ungrouped site/event, or any group-CRUD or
group-key endpoint — gets this same treatment: 404 for "not yours",
403 only for the handful of unscoped-only actions listed in
Group-scoped keys's own error table above.
| Status | When | Message |
|---|---|---|
| 400 | POST /sites with an unparseable domain | invalid domain |
| 400 | POST /sites with a site_url that isn't http(s) | site_url must be an http(s) URL |
| 400 | PATCH /sites/{id} with neither name nor group_id in the body | name or group_id required |
| 400 | DELETE /sites/{id} without ?confirm=true | destructive: pass ?confirm=true — the site, its keys, and its events are removed |
| 400 | GET /events with a severity value outside info, low, medium, high, critical | invalid severity |
| 401 | Missing or invalid Authorization header | (varies) |
| 403 | Account is on the Free plan | api access requires a paid plan |
| 403 | Group CRUD (POST/PATCH/DELETE /groups*) or any group-key endpoint (.../groups/{id}/keys*) called with a group key instead of an unscoped account key — even for its own group | account key required |
| 404 | Site id doesn't exist, or isn't yours | not found |
| 404 | POST /sites, PATCH /sites/{id} group_id, or GET /events group_id, isn't a valid UUID or isn't one of your groups | not found |
| 404 | A group key targeting a site, group, or event outside its own group | not found |
| 409 | POST /sites for a domain you already registered | site already registered |
| 409 | POST /sites or .../activate past your plan's site cap | plan site limit reached |
| 409 | .../activate on a revoked (not paused) site | site cannot be reactivated — contact support |
| 409 | POST /sites/{id}/tokens when 2 keys are already active | two active keys already — revoke one before minting another |
| 409 | POST /groups or PATCH /groups/{id} with a name you already have on another group | group name already exists |
| 409 | POST /groups or PATCH /groups/{id} with an external_ref you already have on another group | external_ref already exists |
| 409 | POST /groups/{id}/keys past that group's 10-active-key cap | ten active group keys already — revoke one before minting another |
| 429 | Over 10 req/s (burst 30) on this key — resellers exempt | rate limited |
Also available: MCP
If you'd rather ask questions than write queries, connect the same account key to MCP and manage sites or query events across your account directly from an AI tool.