Authentication
Choose between anonymous reads, an Organization API key, and an OAuth access token for Sign in with Ulsav.
Each operation accepts one or more of three mechanisms, and they are not interchangeable.
| Mechanism | Credential | Acts as | Typical use |
|---|---|---|---|
| Anonymous | None | Nobody | Discovery and published-content reads |
| Organization API key | ulsav_sk_... | The Organization | Server-to-server integrations and your own backend |
| OAuth access token | Bearer token from Sign in with Ulsav | A signed-in person | Third-party participant frontends |
An operation with no authentication returns public data even when a valid credential is present; that credential is ignored. An invalid API key is still rejected before the handler runs.
Diagram source
flowchart TD
S[Operation declares its authentication] --> P{Public read}
P -->|yes| AN[Anonymous, no credential]
P -->|no| H{Acting for a person}
H -->|no| K[Organization API key ulsav_sk_]
H -->|yes| O[OAuth access token]Bearer credentials
/api/v1/organizations/{organization_id}/eventsList Organization Events, with the key carried in the bearer header.
events.readAPI keys and OAuth access tokens both travel in the Authorization header:
GET /api/v1/organizations/{organization_id}/events HTTP/1.1
Host: api.ulsav.com
Authorization: Bearer ulsav_sk_...
Accept: application/jsonNever put either in a query string, path, or body. URLs land in logs, analytics, and referrer headers, and a leaked credential works until revoked.
Organization API keys carry the ulsav_sk_ prefix; OAuth access tokens carry ulsav_oat_.
Organization API keys
A key is owned by an Organization, not its creator.
- It carries a narrow capability set and one Scope.
- It suits a backend that syncs Orders, exports Registrations, or receives a webhook.
- It cannot act for a person, so it cannot read "my Orders" or consent on anyone's behalf; use OAuth for that.
API keys covers issuance, scoping, rotation, and the capabilities a key can never hold.
OAuth access tokens
Third-party frontends use authorization-code + PKCE and receive user-scoped access tokens, never Ulsav session cookies. The Market apex (ulsav.com) is the authorization server.
| Step | Endpoint |
|---|---|
| Scope catalog | GET /api/v1/oauth/scopes |
| Register a client | POST /api/v1/oauth/clients |
| Authorize | POST /api/v1/oauth/authorizations |
| Exchange the code | POST /api/v1/oauth/token |
| Resolve the person | GET /api/v1/oauth/userinfo |
| Revoke a token | POST /api/v1/oauth/revoke |
The public scope catalog defines the scopes a client may request:
| Scope | Meaning |
|---|---|
profile | The signed-in person's basic profile |
registrations.read | Their Registrations and Participants |
orders.read | Their Orders and receipts |
Register a client
Register a client with exact redirect URIs and scopes. Registration is session-authenticated.
POST /api/v1/oauth/clients
Content-Type: application/json
{
"name": "My Event App",
"redirectUris": ["https://myapp.example/callback"],
"scopes": ["profile", "registrations.read"],
"confidential": false
}Redirect URIs must be https (or http://localhost or http://127.0.0.1), exact, and fragment-free. A confidential client receives a secret once; keep it server-side.
Authorize
Send the person to the apex consent page, or approve server-side with POST /api/v1/oauth/authorizations. That operation requires a signed-in session and carries:
clientId,redirectUri, andscopes- a S256
codeChallenge state
The result is a single-use code with a two-minute lifetime, bound to the client, redirect URI, and PKCE challenge.
Exchange the code
Exchange the code at POST /api/v1/oauth/token with:
grantType: "authorization_code"clientIdandredirectUricodecodeVerifier
The response contains:
| Field | Carries |
|---|---|
accessToken | The bearer token for API calls |
refreshToken | The rotating refresh grant |
tokenType | The scheme for the Authorization header |
expiresIn | The access token's lifetime |
scope | The granted scopes |
Call the API
/api/v1/oauth/userinfoResolve the person behind an OAuth access token; requires the profile scope.
Send the token as Authorization: Bearer ulsav_oat_... to GET /api/v1/oauth/userinfo or a user-scoped resource.
Refresh and revoke
Refresh with grantType: "refresh_token". Refresh tokens rotate on every use, and a superseded token revokes the family. Confidential clients also send clientSecret.
Revoke either token at POST /api/v1/oauth/revoke.
CORS and browser clients
CORS has two tiers that never mix.
| Tier | Allowed origins | Credential |
|---|---|---|
| Credentialed (cookie) | Ulsav's platform hosts, *.ulsav.com, and verified active custom domains | Cookies |
| Public API | An exact scheme://host[:port] from the key's allowedOrigins, otherwise the platform public allowlist, otherwise any https:// origin | Bearer header |
A third-party origin cannot use cookies, and a wildcard is never combined with credentials. For a key that declares allowedOrigins, the browser origin must match one exactly, with no path or wildcard.
A key with an empty list inherits the platform allowlist. When both lists are empty, any https:// origin is reflected, because the request carries no cookies.
A cross-origin call with no Authorization header gets CORS permission only from an allowlisted platform or verified custom-domain origin. Anonymous reads are for servers, crawlers, and same-origin pages. Disallowed preflight requests are rejected.
Sessions and host handoff
A person moving to an EventGroup subdomain or verified custom domain goes through the same server: the host exchanges a short-lived, single-use code for its own host-scoped session family, and signing out revokes only that family. A third-party frontend gets tokens, never a Ulsav session.
Match the credential to the operation
Some operations are session-only; API-key management is the clearest example. A key is rejected there regardless of its capabilities. If an operation returns api_key_forbidden with "this operation does not accept API keys", use a signed-in session or an OAuth token.