Extension integration contract
Production domain: https://ghlcloner.autogencrm.com. Base: /api/v1. CORS is restricted to configured Chrome extension IDs and localhost. All extension endpoints return { ok: true, ... } or { ok: false, error: { code, message, details? } }.
Extension lifecycle (summary)
- Activate: the Chrome popup asks for a license key only — no account password. It sends the key plus a random per-install
device_id(UUID minted bycrypto.randomUUID()and persisted inchrome.storage.local) toPOST /api/v1/extension/license/activate. The server hashes the key, atomically binds this device to the license (respecting the plan's device limit), and issues a short-lived access token plus a rotating refresh token — exactly the same token shape as the legacy login. - Rotating tokens: every
/auth/refreshcall issues a new access + refresh pair and revokes the old refresh token; reusing a revoked refresh token revokes all sibling sessions for the device. - Authorize a native clone: extension calls
POST /api/v1/extension/usage/authorizewith an idempotency key and source/destination funnel-step IDs; the server atomically debits one credit and returns ausage_id. - GHL native clone runs client-side inside the tab. On success, extension calls
POST /api/v1/extension/eventswithtype: "clone.completed"and theusage_idto finalize the record. - On GHL failure, extension calls
POST /api/v1/extension/usage/reversewith theusage_idto refund the credit (idempotent within 10 minutes). - Deactivate:
POST /api/v1/extension/license/deactivate(or/auth/logout) revokes the current session and releases the device slot on this install.
GHL credentials, cookies, and page markup never leave the browser — only IDs, URLs, extension version, and diagnostics are sent to us. Email/password login on the website is for the dashboard only; admin controls appear inside that dashboard for admin users.
Required headers
Content-Type: application/jsonon all POST/DELETE.Authorization: Bearer <access_token>on every endpoint except/license/activateand/auth/refresh.X-Device-Id(required on/license/activate): MUST equalbody.device_id.X-Idempotency-Key(optional; body fieldidempotency_keyalso accepted) on/usage/authorize.X-Extension-Version(optional): forwarded into audit metadata.
Device identity
The extension MUST generate a per-install random UUID (crypto.randomUUID()) and persist it in chrome.storage.local. This device_id is sent on /license/activate; the server stores a peppered SHA-256 hash. No hardware fingerprinting.
License activation
License keys are canonical uppercase keys in this format: GHLPC-XXXX-XXXX-XXXX-XXXX-XXXX, using only non-confusing characters (A-Z except I/O, plus 2-9). Users and admins can copy/view the full key only at issue/rotation time in the dashboard; later the dashboard shows only a masked key identifier and offers a secure Check key action for admins that hashes the pasted key locally and verifies it against the stored hash.
POST /api/v1/extension/license/activate
Headers: X-Device-Id: <uuid must equal body.device_id>
{
"license_key": "GHLPC-XXXX-XXXX-XXXX-XXXX-XXXX",
"device_id": "<uuid-v4 minted by the extension>",
"device_label": "Chrome on macOS",
"extension_version": "1.3.0"
}Returns access_token (15 min), refresh_token (30 days), session_id, license_id — identical shape to the previous login response, so v1.3 extension storage/refresh/me/usage code needs no changes. Error codes (deliberately generic to prevent enumeration): invalid_license, license_revoked, license_expired, device_limit_reached, pending_approval, account_suspended, device_id_mismatch, rate_limited.
The plaintext license key is shown exactly once at issuance/rotation and never stored on the server — only a SHA-256 hash, the prefix, and last-4 characters are kept for display. Masked dots are identifiers only, not usable activation keys.
POST /api/v1/extension/license/deactivate (auth)
Revokes the current session and releases this device slot from the license. Idempotent.
Auth (legacy — website only)
POST /api/v1/extension/auth/login
Email + password login retained for the website dashboard only. The Chrome extension MUST use /license/activate.
POST /api/v1/extension/auth/refresh
{ "refresh_token": "..." }Refresh tokens are rotated on every call and preserve the bound license + device. Reusing a revoked token revokes every sibling session for the device. Additional error codes: refresh_revoked, refresh_expired, license_*, device_revoked.
POST /api/v1/extension/auth/logout
Revokes the current session (device binding is preserved).
License & devices
GET /api/v1/extension/license
Returns masked key, status, expiry, max/active devices, current device id.
DELETE /api/v1/extension/devices/current
Revokes the current device binding and all its sessions. POST is accepted as an alias.
GET /api/v1/extension/me
Aggregate: user, subscription with plan (including max_devices), license summary, current device id.
Cloning
POST /api/v1/extension/usage/authorize
{
"idempotency_key": "uuid-per-clone-attempt",
"mode": "native",
"source": { "funnel_id": "...", "step_id": "...", "url": "https://..." },
"destination": { "funnel_id": "...", "step_id": "..." },
"extension_version": "1.0.0"
}Atomically validates active profile, subscription, license, bound device, then debits 1 credit (0 on unlimited plans). Response includes usage_id and remaining balance. Do NOT include GHL tokens, page markup, or funnel bodies — only IDs and URLs.
POST /api/v1/extension/usage/reverse
{ "usage_id": "...", "reason": "ghl-native-clone-failed" }Idempotent refund within 10 minutes of authorization.
POST /api/v1/extension/events
{ "type": "clone.completed", "usage_id": "...", "metadata": { "duration_ms": 1234 } }Fallback converter
When native cloning is unavailable, the extension may POST the scraped payload to /api/public/convert with the same Authorization: Bearer access token. Requires an active subscription and consumes one credit.
Standard error codes
missing_token | invalid_token | legacy_token | session_missing | session_revoked | session_expired
account_suspended | license_missing | license_revoked | license_expired | device_revoked | device_limit_reached
invalid_credentials | rate_limited | invalid_json | invalid_body
no_subscription | subscription_inactive | insufficient_credits | reversal_window_expiredEnvironment
EXTENSION_JWT_SECRET— HS256 secret for access tokens (also peppers device-id hashes).EXTENSION_ALLOWED_ORIGINS— optional comma-separated extension origins.- Extension IDs and the
allow_unlisted_unpacked_extensionsdev flag are editable in the admin console.