GrowthOS uses two distinct API key types, each designed for a specific trust boundary.
Write Key
Prefix: gos_wk_
Client-safe. Embed directly in your JavaScript SDK, browser code, or mobile app. Write keys can only call Ingest API endpoints — they cannot read data, modify resources, or access admin functions.
Auto-generated when you create a project. One write key per project by default.
Secret Key
Prefix: gos_sk_
Server-side only. Never expose in client-side code, public repos, or frontend bundles. Secret keys call the Management API and are created with granular permission scopes.
You can create multiple secret keys per project, each with different scopes for different services.
The Ingest API accepts write keys via header or query parameter .
Option 1 — Authorization header (recommended):
Authorization : Bearer gos_wk_your_write_key
Content-Type : application/json
"event" : " Signup Completed " ,
Option 2 — Query parameter:
POST /v1/track?writeKey=gos_wk_your_write_key
The Management API accepts secret keys via header only . Query parameter auth is not supported for security reasons.
GET /v1/contacts?limit=50 HTTP / 1.1
Authorization : Bearer gos_sk_your_secret_key
Content-Type : application/json
Secret keys are created with granular scopes that control which resources and operations the key can access. Follow the principle of least privilege — grant only the scopes each service needs.
Scope Permission contacts:readView contacts and their properties contacts:writeCreate, update, and delete contacts events:readQuery event history and raw event data campaigns:readView email campaigns and their status campaigns:writeCreate, update, send, and archive campaigns referrals:readView referral programs and participant data referrals:writeCreate and modify referral programs segments:readView segment definitions and membership segments:writeCreate, update, and delete segments surveys:readView surveys and response data surveys:writeCreate, update, and publish surveys waitlists:readView waitlists and subscriber data waitlists:writeCreate, manage, and promote waitlist entries webhooks:readView webhook subscriptions webhooks:writeCreate, update, and delete webhook subscriptions analytics:readQuery analytics dashboards and reports settings:readView project settings and configuration settings:writeModify project settings billing:readView billing information and usage *Full access (admin) — grants all scopes
"scopes" : [ " contacts:read " , " campaigns:read " , " campaigns:write " ]
Response:
"key" : " gos_sk_live_abc123... " ,
"scopes" : [ " contacts:read " , " campaigns:read " , " campaigns:write " ],
"created_at" : " 2026-02-23T10:00:00Z "
If a key attempts an operation outside its scopes, the API returns 403 Forbidden:
"code" : " insufficient_scope " ,
"message" : " This API key does not have the 'contacts:write' scope required for this operation. " ,
"required_scope" : " contacts:write " ,
"granted_scopes" : [ " contacts:read " , " campaigns:read " , " campaigns:write " ]
"request_id" : " req_9z3n5p7q "
GrowthOS enforces strict tenant isolation at the database level using PostgreSQL Row-Level Security (RLS) .
Every API request includes an API key (write key or secret key)
The API gateway resolves the key to a tenant_id
The database session variable app.current_tenant is set before any query executes
RLS policies on every table enforce WHERE tenant_id = current_setting('app.current_tenant') automatically
Results are guaranteed to contain only the requesting tenant’s data
No cross-tenant leaks
Even a SQL injection vulnerability cannot access another tenant’s data — the RLS policy is enforced by PostgreSQL itself, not application code.
Audit trail
Every API request is logged with tenant_id, api_key_id, and request_id for full traceability.
GrowthOS signs all outbound webhook payloads with HMAC-SHA256 so you can verify they originated from GrowthOS and have not been tampered with.
Header Description X-GrowthOS-SignatureHMAC-SHA256 hex digest of the signed payload X-GrowthOS-TimestampUnix timestamp (seconds) when the webhook was sent
Extract the X-GrowthOS-Timestamp and X-GrowthOS-Signature headers
Concatenate: timestamp + "." + raw_request_body
Compute HMAC-SHA256 using your webhook endpoint’s signing secret
Compare the computed signature with X-GrowthOS-Signature using a timing-safe comparison
Reject if the timestamp is older than 5 minutes (replay protection)
import crypto from ' crypto ' ;
function verifyWebhook ( req , signingSecret ) {
const signature = req . headers [ ' x-growthos-signature ' ];
const timestamp = req . headers [ ' x-growthos-timestamp ' ];
const body = req . rawBody ; // raw request body as string
// Replay protection: reject if older than 5 minutes
const age = Math . floor ( Date . now () / 1000 ) - parseInt ( timestamp , 10 );
throw new Error ( ' Webhook timestamp too old — possible replay attack ' );
// Compute expected signature
const payload = ` ${ timestamp } . ${ body } ` ;
. createHmac ( ' sha256 ' , signingSecret )
// Timing-safe comparison
const isValid = crypto . timingSafeEqual (
throw new Error ( ' Invalid webhook signature ' );
def verify_webhook ( headers , body , signing_secret ) :
signature = headers[ ' X-GrowthOS-Signature ' ]
timestamp = headers[ ' X-GrowthOS-Timestamp ' ]
# Replay protection: reject if older than 5 minutes
age = int ( time. time ()) - int ( timestamp )
raise ValueError ( ' Webhook timestamp too old ' )
# Compute expected signature
payload = f " { timestamp } . { body } "
if not hmac. compare_digest ( signature , expected ):
raise ValueError ( ' Invalid webhook signature ' )
Enterprise
For organizations with strict network security requirements, Management API access can be restricted to specific IP addresses or CIDR ranges.
POST /v 1 /settings/ip-allowlist
When IP allowlisting is enabled:
Requests to the Management API from non-allowed IPs receive 403 Forbidden
The Ingest API is unaffected — client-side tracking continues to work from any IP
Dashboard access can optionally be restricted to the same allowlist
Rotating API keys without downtime follows a three-step process.
Create a new key in the dashboard or via the API. The new key is active immediately
Update your integrations to use the new key. Deploy the changes across all services that reference the old key
Revoke the old key in the dashboard. A 24-hour grace period begins — the old key continues to work during this window to cover staggered deployments
Scheduled rotation
Set up automatic key rotation on a schedule (30, 60, or 90 days) in project settings. The system creates the new key and sends a webhook notification — your CI/CD pipeline handles the swap.
Emergency revocation
If a key is compromised, you can revoke it immediately with no grace period . This is a hard cut — all requests with the revoked key fail instantly.