Brands API
Brands live under organizations. All routes require
Authorization: Bearer <access_token> with the appropriate
IdentityOrgPermission granted on the parent org.
:orgSlug/:brandSlug identifies the brand. Both slugs are stable.
Create a brand
POST /identity/organizations/:orgSlug/brands
Requires org.brands.edit.
{
slug: string // [a-z0-9-]{3,30}, unique within the org
name: string // 1–80 chars, shown on consent screen
primaryColor?: string // hex, e.g. '#5FD8CA'
supportEmail?: string
privacyPolicyUrl?: string
termsUrl?: string
logoFile?: string // FileTracker id
providerOrder?: string[] // default order in sign-in modal
}Response (201): the created Brand.
oauthCallbackId is generated by Synq and returned in the
response. It never changes after creation; the callback URL pattern
it appears in (https://synq.id/oauth/<provider>/<oauthCallbackId>/callback)
is what you register with each OAuth provider.
Read / list / update / delete
List brands
GET /identity/organizations/:orgSlug/brands — requires org.brands.view.
Get one
GET /identity/organizations/:orgSlug/brands/:brandSlug
interface Brand {
id: string
org: string
slug: string
name: string
oauthCallbackId: string // immutable
logoFile?: FileTracker
primaryColor?: string
supportEmail?: string
privacyPolicyUrl?: string
termsUrl?: string
providerOrder: string[]
createdAt: string
updatedAt: string
}Update
PATCH /identity/organizations/:orgSlug/brands/:brandSlug — requires
org.brands.edit.
{
name?: string
primaryColor?: string | null
supportEmail?: string | null
privacyPolicyUrl?: string | null
termsUrl?: string | null
logoFile?: string | null
providerOrder?: string[]
}slug and oauthCallbackId cannot be changed.
Delete
DELETE /identity/organizations/:orgSlug/brands/:brandSlug — requires
org.brands.edit.
Soft-deletes. Apps and connections under the brand are disabled.
Apps under a brand
List apps
GET /identity/organizations/:orgSlug/brands/:brandSlug/apps
Requires org.apps.view.
Returns { items: App[], total: number }.
Create an app
POST /identity/organizations/:orgSlug/brands/:brandSlug/apps
Requires org.apps.edit.
{
name: string // 1–80 chars
type: 'web' | 'spa' | 'native' | 'm2m' | 'agent'
isConfidential?: boolean // default: true for web/m2m, false otherwise
redirectUris: string[] // required for browser flows
allowedGrantTypes: GrantType[]
allowedScopes: string[]
description?: string
}
type GrantType =
| 'authorization_code'
| 'refresh_token'
| 'client_credentials'
| 'urn:ietf:params:oauth:grant-type:device_code'Response (201): the new App, including clientSecret for
confidential clients. clientSecret is returned only on create
and on /rotate-secret.
interface App {
id: string
brand: string
org: string
clientId: string
clientSecret?: string // only on create + rotate
name: string
type: 'web' | 'spa' | 'native' | 'm2m' | 'agent'
isConfidential: boolean
redirectUris: string[]
allowedGrantTypes: GrantType[]
allowedScopes: string[]
enabled: boolean
description?: string | null
createdAt: string
updatedAt: string
}Get an app
GET /identity/organizations/:orgSlug/brands/:brandSlug/apps/:clientId
Requires org.apps.view. No clientSecret in the response.
Update an app
PATCH /identity/organizations/:orgSlug/brands/:brandSlug/apps/:clientId
Requires org.apps.edit.
{
name?: string
description?: string | null
redirectUris?: string[]
allowedGrantTypes?: GrantType[]
allowedScopes?: string[]
enabled?: boolean
}Rotate the client_secret
POST /identity/organizations/:orgSlug/brands/:brandSlug/apps/:clientId/rotate-secret
Requires org.apps.edit. Issues a new clientSecret and invalidates
the previous one. The previous secret stops working immediately.
Response: { clientSecret: string } — returned once.
Delete an app
DELETE /identity/organizations/:orgSlug/brands/:brandSlug/apps/:clientId
Soft-disables. Outstanding access tokens issued by this App stop validating on their next request to the issuer.
Connections (BYO credentials)
Upsert a connection
PUT /identity/organizations/:orgSlug/brands/:brandSlug/connections
Requires org.connections.edit.
{
type: 'google' | 'apple' | 'microsoft' | 'discord'
| 'facebook' | 'x' | 'telegram' | 'matrica'
enabled?: boolean // default: true
config: ProviderConfig // shape varies per provider
}ProviderConfig shapes — see
BYO credentials for full details.
Response: connection metadata (no plaintext config — encrypted at rest).
{
id: string
brand: string
type: string
enabled: boolean
callbackUrl: string // the URL to register with the provider
createdAt: string
updatedAt: string
}Toggle enabled
PATCH /identity/organizations/:orgSlug/brands/:brandSlug/connections/:type/enabled
{ enabled: boolean }Delete a connection
DELETE /identity/organizations/:orgSlug/brands/:brandSlug/connections/:type
Reverts the brand to Synq default credentials for that provider (if any).
List connections
GET /identity/organizations/:orgSlug/brands/:brandSlug/connections
Requires org.connections.view. Returns metadata only — never
the encrypted config or plaintext fields.
Scopes vocabulary
List
GET /identity/organizations/:orgSlug/brands/:brandSlug/scopes
Returns the brand-specific scope vocabulary. Does not include the OIDC standard scopes (those are universal).
{
items: Array<{
name: string // e.g. 'wallets:read'
description: string // shown on consent screen
createdAt: string
}>
}Add
POST /identity/organizations/:orgSlug/brands/:brandSlug/scopes
Requires org.scopes.edit.
{ name: string; description: string }name must be [a-z0-9:_-]{2,80}. Convention: resource:action.
Update description
PATCH /identity/organizations/:orgSlug/brands/:brandSlug/scopes/:name
{ description: string }The name itself cannot be edited (it appears in token claims).
Delete
DELETE /identity/organizations/:orgSlug/brands/:brandSlug/scopes/:name
Returns conflict if any App under the brand currently lists this
scope in its allowedScopes. Remove from those Apps first.
Errors
Standard REST envelope. See Errors.
forbidden— missingorg.brands.*/org.apps.*/org.connections.*/org.scopes.*permission.conflict— slug, scope name, or callback URL collision.validation_failed— body validation.not_found— brand or app slug does not exist (or you cannot see it).