Ulsav Docs

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.

MechanismCredentialActs asTypical use
AnonymousNoneNobodyDiscovery and published-content reads
Organization API keyulsav_sk_...The OrganizationServer-to-server integrations and your own backend
OAuth access tokenBearer token from Sign in with UlsavA signed-in personThird-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

GET/api/v1/organizations/{organization_id}/events

List Organization Events, with the key carried in the bearer header.

Auth API keyCapability events.read

API 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/json

Never 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.

StepEndpoint
Scope catalogGET /api/v1/oauth/scopes
Register a clientPOST /api/v1/oauth/clients
AuthorizePOST /api/v1/oauth/authorizations
Exchange the codePOST /api/v1/oauth/token
Resolve the personGET /api/v1/oauth/userinfo
Revoke a tokenPOST /api/v1/oauth/revoke

The public scope catalog defines the scopes a client may request:

ScopeMeaning
profileThe signed-in person's basic profile
registrations.readTheir Registrations and Participants
orders.readTheir 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, and scopes
  • 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"
  • clientId and redirectUri
  • code
  • codeVerifier

The response contains:

FieldCarries
accessTokenThe bearer token for API calls
refreshTokenThe rotating refresh grant
tokenTypeThe scheme for the Authorization header
expiresInThe access token's lifetime
scopeThe granted scopes

Call the API

GET/api/v1/oauth/userinfo

Resolve 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.

TierAllowed originsCredential
Credentialed (cookie)Ulsav's platform hosts, *.ulsav.com, and verified active custom domainsCookies
Public APIAn exact scheme://host[:port] from the key's allowedOrigins, otherwise the platform public allowlist, otherwise any https:// originBearer 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.

On this page