Authentication

Two key types, one principle - secrets stay on the server, public keys go in the browser.

Two key types

TypePrefixWhere it livesUse for
Secret key sk_test_…, sk_live_… Your server (env var, secrets manager) Server-to-server API calls
Public widget key pk_test_…, pk_live_… Your customer's website HTML Embedded widgets that visitors interact with
Never put sk_… in browser code. Anyone with a secret key can read/write your tenant's data.

Secret keys

Create in the dashboard → service → DevelopersCreate key. Pick a name, environment, scopes.

Use with a Bearer header:

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx

You'll see the full secret once at creation. Copy it to your secrets manager immediately - we don't store it in plaintext (SHA-256 hashed at rest).

Public keys

Issued per-service in the dashboard, locked to an origin allowlist. Used by the widget loader.

<script src="https://app.softsolz.uk/softsolz.js"
  data-public-key="pk_live_xxxxxxxxxxxx"
  data-service-id="forms"
  data-form-slug="contact-us"></script>

A request from a non-allowlisted origin is rejected - even with a stolen key, an attacker can't load your widget on their site.

Scopes

Each key carries a list of scopes like service.forms.submit. The platform checks each call's required scope against the key's set.

Environments

Every workspace has a paired sandbox. Sandbox keys (sk_test_… / pk_test_…) write to the sandbox tenant. No real Stripe / email side-effects ever fire in sandbox.

The same base URL serves both - the key prefix decides which tenant the request lands on.

Rotation

Two paths:

Auth errors

StatusBodyMeaning
401missing_authorizationNo Bearer header.
401invalid_api_keyKey revoked or unknown.
403insufficient_scopeKey doesn't have the required scope.
403origin_not_allowedPublic key called from a non-allowlisted origin.
402service_inactiveThe service subscription lapsed.
429rate_limit_exceededSlow down - check X-RateLimit-Remaining.