Skip to Content
Authentication

Authentication

This page covers End-User API authentication. For programmatic organization management (the Management API), see API Keys and Management API.

TokenPurposeLifetimeIssued by
End-user Access Token (PL-JWT)Auth header for all End-User API calls12h (default)/auth/user/sign-in, /auth/user/register
Refresh TokenObtain new access tokens30d (rotating, one-time use)/auth/user/sign-in, /auth/token/refresh
Publishable key (pk_*)Create ephemeral sessions for client apps5 min (token)MintEphemeral action
Secret key (sk_live_*)Management API — programmatic org management1h (token)ExchangeSecretKey mutation

Required headers

Authorization: Bearer <PL-JWT> Content-Type: application/json X-Request-Id: <uuid> (optional – auto-generated if missing)

Auth endpoints

All auth routes live under /auth/* and do not require authentication.

EndpointDescription
POST /auth/user/registerCreate a new app user (handler, password, optional profile). Returns access+refresh tokens.
POST /auth/user/sign-inExchange handler + password for tokens.
POST /auth/token/refreshRotate refresh tokens using body { \"refreshToken\": \"...\" }; returns new { token, refreshToken }. Legacy { \"token\": \"...\" } is rejected.
POST /auth/user/sign-outRevoke refresh token using body { \"refreshToken\": \"...\" } (idempotent).
POST /auth/reset-password/requestSend password reset email/SMS (always 200).
POST /auth/reset-password/confirmComplete reset with token + new password.

All responses follow the standard envelope: { code, message, type, data }.

Social sign-in (Apple / Google)

In addition to password sign-in, PickLayer exposes two social sign-in endpoints that accept a provider-issued identity token, verify it against the provider’s JWKS, and mint the same { token, refreshToken, user } envelope as /auth/user/sign-in.

Each provider requires its client ID to be set on the backend. If it is missing, the endpoint returns 503 with code: provider_not_configured and logs auth.social_provider_not_configured at ERROR — misconfiguration is surfaced loudly instead of being hidden behind a 404.

ProviderRequired config
AppleAPPLE_CLIENT_ID (bundle or service ID)
GoogleGOOGLE_CLIENT_ID

Both routes share the /auth/user/sign-in rate-limit bucket: 5 attempts per minute per (IP, handler) pair; overflow returns 429 with a Retry-After header.

Endpoints

EndpointBodyNotes
POST /auth/user/sign-in/apple{ identityToken, nonce, authorizationCode?, profile? }identityToken is the JWT Apple returns; nonce is the plaintext nonce the client passed to Apple at sign-in (the token’s nonce claim is a SHA-256 hash of it). authorizationCode is logged but not exchanged in v1.
POST /auth/user/sign-in/google{ idToken, profile? }idToken is the JWT returned by Google Identity Services.

Successful responses follow the standard envelope with data: { token, refreshToken, user } — identical to /auth/user/sign-in. First-time sign-in auto-creates a user record keyed by the provider’s stable sub claim; subsequent sign-ins resolve the existing user. Existing password-registered users are linked automatically when the provider-verified email matches their handler — credentials are never mutated during linking.

Error codes

StatusMeaning
400Missing identityToken/nonce/idToken, or the provider token had no email claim.
401Provider token signature invalid, expired, or aud/iss mismatch.
403Account exists but is suspended (active=false).
429Rate limit exceeded — honor Retry-After.
503Either provider JWKS temporarily unavailable (code: provider_unavailable), or the provider’s client ID is unset on the backend (code: provider_not_configured).

Python SDK

from picklayer import PickLayerClient client = PickLayerClient(org_handler="acme", app_handler="myapp") # Apple (nonce is the plaintext nonce you passed to Apple) resp = client.sign_in_apple( identity_token=apple_identity_token, nonce=plaintext_nonce, authorization_code=apple_authorization_code, # optional ) # Google resp = client.sign_in_google(id_token=google_id_token) access_token = resp.data["token"] refresh_token = resp.data["refreshToken"]

React / TypeScript SDK

import { PickLayerClient } from "@picklayer/sdk"; const client = new PickLayerClient({ orgHandler: "acme", appHandler: "myapp" }); const apple = await client.signInApple(appleIdentityToken, plaintextNonce, { authorizationCode: appleAuthorizationCode, // optional }); const google = await client.signInGoogle(googleIdToken);

Flutter / Dart SDK

final apple = await client.signInApple( identityToken: appleIdentityToken, nonce: plaintextNonce, authorizationCode: appleAuthorizationCode, // optional ); final google = await client.signInGoogle(idToken: googleIdToken);

Preview environment authentication

PickLayer deployments can be protected by a preview gate that requires additional credentials on every API request. When enabled, requests without valid preview credentials receive a 401 response with "Preview credentials required.".

Preview auth is separate from — and does not replace — the Authorization: Bearer <PL-JWT> header used for end-user authentication. Both headers are sent together.

How it works

When you access the PickLayer app in a browser, a CloudFront viewer-response function sets a previewAuth cookie containing your encoded credentials. The frontend automatically reads this cookie and attaches the X-Preview-Basic-Auth header to all API requests.

For direct API calls (cURL, SDKs, server-to-server), you must pass the preview credentials yourself.

Supported headers

HeaderFormatExample
X-Preview-Basic-AuthBasic <base64(username:password)>Basic cHJldmlldzpteS1zZWNyZXQ=
X-Preview-Token<secret>my-preview-token

You can also pass credentials via Authorization: Basic <base64> if no Bearer token is needed on that request, or via query parameter ?previewToken=<secret> for WebSocket connections.

cURL example

# Encode credentials PREVIEW_HASH=$(echo -n "preview:my-secret" | base64) # Include alongside your Bearer token curl https://api.picklayer.ai/api/org/acme/app/myapp/v1/chat/completions \ -H "Authorization: Bearer $PL_JWT" \ -H "X-Preview-Basic-Auth: Basic $PREVIEW_HASH" \ -H "Content-Type: application/json" \ -d '{"model":"openai/gpt-4o","messages":[{"role":"user","content":"Hello"}]}'

Python SDK

The PickLayer Python SDK supports preview auth at initialization or after:

from picklayer import PickLayerClient # Set at initialization client = PickLayerClient( org_handler="acme", app_handler="myapp", base_url="https://api.picklayer.ai", preview_basic_auth="preview:my-secret", ) # Or set after initialization client.set_preview_auth(username="preview", password="my-secret") # Or use a gate token instead client.set_preview_auth(gate_token="my-preview-token") # Clear preview credentials client.clear_preview_auth()

Exempt paths

The following paths do not require preview credentials:

  • /api/healthz — health check
  • /billing/webhook — Stripe webhooks
  • /openapi, /openapi/ui — API documentation
  • File download URLs with signed tokens
Last updated on