Authentication
This page covers End-User API authentication. For programmatic organization management (the Management API), see API Keys and Management API.
| Token | Purpose | Lifetime | Issued by |
|---|---|---|---|
| End-user Access Token (PL-JWT) | Auth header for all End-User API calls | 12h (default) | /auth/user/sign-in, /auth/user/register |
| Refresh Token | Obtain new access tokens | 30d (rotating, one-time use) | /auth/user/sign-in, /auth/token/refresh |
Publishable key (pk_*) | Create ephemeral sessions for client apps | 5 min (token) | MintEphemeral action |
Secret key (sk_live_*) | Management API — programmatic org management | 1h (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.
| Endpoint | Description |
|---|---|
POST /auth/user/register | Create a new app user (handler, password, optional profile). Returns access+refresh tokens. |
POST /auth/user/sign-in | Exchange handler + password for tokens. |
POST /auth/token/refresh | Rotate refresh tokens using body { \"refreshToken\": \"...\" }; returns new { token, refreshToken }. Legacy { \"token\": \"...\" } is rejected. |
POST /auth/user/sign-out | Revoke refresh token using body { \"refreshToken\": \"...\" } (idempotent). |
POST /auth/reset-password/request | Send password reset email/SMS (always 200). |
POST /auth/reset-password/confirm | Complete 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.
| Provider | Required config |
|---|---|
| Apple | APPLE_CLIENT_ID (bundle or service ID) |
GOOGLE_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
| Endpoint | Body | Notes |
|---|---|---|
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
| Status | Meaning |
|---|---|
400 | Missing identityToken/nonce/idToken, or the provider token had no email claim. |
401 | Provider token signature invalid, expired, or aud/iss mismatch. |
403 | Account exists but is suspended (active=false). |
429 | Rate limit exceeded — honor Retry-After. |
503 | Either 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
| Header | Format | Example |
|---|---|---|
X-Preview-Basic-Auth | Basic <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