Every endpoint on this page is built, deployed, and acceptance-tested against real HTTP traffic — not a design sketch. Base URL: https://api.jacai.ai.
Plugin/content endpoints (/ingest) use a per-site signed-request scheme. Everything else (/auth, /account, /admin) uses a login session — a cookie for a browser, a Bearer token for everything else.
site_id — public identifier for the connected WordPress site, issued once when the site is provisioned.site_secret — random secret, generated on connect, never re-shown.
Every request carries three headers: X-JacAI-Site, X-JacAI-Timestamp, X-JacAI-Signature. The signature is HMAC-SHA256( timestamp + "\nPOST\n" + path + "\n" + sha256(body), site_secret ), hex-encoded. Requests outside the configured timestamp tolerance (5 minutes by default) are rejected, and an identical signed request replayed a second time is rejected too — signature claims are recorded and checked.
POST /auth/login sets an HttpOnly jacai_session cookie for browser callers and returns a CSRF token. Non-browser clients that need a raw Bearer token request one explicitly with X-JacAI-Token-Response: 1.
A non-browser client sends that same token as a Bearer token instead of a cookie: Authorization: Bearer <session_token>. Both paths resolve to the same session — there's no separate API-key system yet.
{
"ok": true,
"service": "api.jacai.ai",
"phase": "ingest-slice + auth-credit-system",
"db": "connected",
"time": "2026-08-25T05:42:09+00:00"
}
Returns 503 with "db":"unavailable" if the database connection fails — safe to poll for uptime monitoring (see status.jacai.ai).
Relevant Bot scans a customer's WordPress site — posts, pages, WooCommerce products — and pushes normalized content items here. Re-sending an unchanged item is a no-op; a changed content_hash updates it in place.
Body — an array of content items:
[
{
"id": "wp-382",
"type": "post",
"title": "How to Repair a Leaking Gutter",
"url": "https://example.com/post",
"excerpt": "...",
"content": "...",
"categories": ["Gutter Repair"],
"tags": ["DIY", "Rain Gutters"],
"language": "en"
}
]
Response:
{
"ok": true,
"received": 1,
"created": 1,
"updated": 0,
"unchanged": 0,
"skipped": 0,
"items": [
{ "external_id": "wp-382", "status": "created" }
]
}
Rate-limited per site (240 requests/minute). A replayed request with an already-seen signature returns 401; a missing or invalid signature also returns 401.
Body: { email, password, name, org_name?, plan_code? }. Creates the user, an organization they own, grants the plan's signup-bonus credits if any, and logs them in immediately.
{
"ok": true,
"user": { "id": 1, "email": "...", "name": "..." },
"organization": { "id": 1, "name": "...", "plan": "starter" },
"csrf_token": "..."
}
Body: { email, password }. Wrong email and wrong password return the identical 401 message — no account-enumeration leak.
{
"ok": true,
"user": { "id": 1, "email": "...", "is_platform_admin": false },
"organizations": [{ "organization_id": 1, "role": "owner" }],
"csrf_token": "..."
}
Revokes the current session only (not every session on the account). Response: { "ok": true }.
Body: { email }. Always returns 200 with a generic message, whether or not the account exists.
{ "ok": true, "message": "If that email has an account, a reset link has been sent." }
Body: { token, new_password }. Consumes the token, sets the new password, and revokes every other live session on the account — a reset is treated as a signal the old sessions shouldn't be trusted.
What the WordPress plugins and any future dashboard call before allowing an action — the caller's resolved plan, limits, and credit balance for one organization.
| Endpoint | What it returns |
|---|---|
GET/account | The logged-in user plus every organization they belong to and their role in each. |
GET/account/entitlements?organization_id= | Resolved plan limits — sites, social accounts, users, languages/campaign, OAuth platforms, BYOK/API access — plus credit_balance. Omit organization_id to use the caller's first membership. |
GET/account/usage?organization_id=&limit= | Recent credit-ledger entries and usage events for the account dashboard's usage tab. limit caps at 200. |
Every admin endpoint is org-scoped by default: pass organization_id and you need owner/admin on that org. Omit it and you need to be a platform admin — a flag reserved for JacAI's own operator account, set by hand in the database, never through an endpoint. An org's own "master" role never becomes a platform-wide superadmin.
| Endpoint | What it does |
|---|---|
GET/admin/overview[?organization_id=] | Platform totals (orgs, users, credits outstanding by plan) or one org's snapshot. |
GET/admin/users[?organization_id=] | List users platform-wide or within one org. |
POST/admin/users/edit | Body: { user_id, organization_id?, role?, status?, is_platform_admin? } — each field independently scope-checked. Only an org owner can promote someone else to owner; only a platform admin can suspend an account or grant the platform-admin flag. |
GET/admin/transactions?organization_id=&kind=&limit=&offset= | The full, filterable credit ledger. |
POST/admin/transfer | Body: { from_organization_id, to_organization_id, credits, note? } — zero-sum org-to-org credit transfer. |
POST/admin/mint | Body: { organization_id, credits, revenue_category_id, note } — platform-admin only. Grants extra credits (promo, comp, referral bonus, billing correction), always tagged to a reason. |
GET / POST/admin/categories | List or create the reason tags /admin/mint grants are attributed to. |
Every error response is JSON with the same shape, and the HTTP status carries the real meaning — this API doesn't return 200 with an error buried in the body.
{ "error": "unauthorized", "message": "invalid email or password" }
| Status | Meaning |
|---|---|
400 | Bad request — missing or invalid fields. |
401 | Missing, invalid, expired, or revoked session/signature. |
403 | Authenticated, but not allowed to do this (wrong org role, not a platform admin, suspended account). |
404 | The referenced resource doesn't exist. |
409 | Conflict — e.g. an account already exists with that email. |
422 | Understood the request, but it can't be processed as given (e.g. unknown plan code, insufficient balance). |
503 | Database temporarily unavailable — see /health. |