openapi: 3.1.0
info:
title: CI HUB Access SDK API
version: v1
summary: 'Embed CI HUB asset connectivity into a partner platform: exchange a partner-signed JWT for
a CI HUB session, connect an end user to a DAM provider, then browse, search and read assets from
that DAM over one uniform contract.'
description: 'The CI HUB Access SDK API is the HTTP surface a partner platform calls to reach any DAM,
MAM, PIM, cloud-storage or work-management system CI HUB connects to, without integrating each one
separately.
Authentication is a token exchange: the partner backend signs an RS256 JWT for the user and exchanges
it at `POST /auth/exchangeToken` for a CI HUB access token (1 hour) and refresh token (30 days). The
end user then connects a DAM provider through `POST /auth/login`, which returns a redirect URI plus
a `state` the partner polls at `GET /auth/login`. From that point every content call carries two tokens:
the CI HUB access token in `Authorization` and the DAM connection token in `provider-authorization`.
Content is read-only in this release: folder browse, keyword search, similarity search by reference
image, asset detail and asset version history. Every failure returns one error envelope whose `error.source`
separates CI HUB platform faults (`cihub`) from DAM provider faults (`integration`).
The API is served under the `/api/v1` prefix and is not versioned beyond it; additive changes ship
in place, breaking changes are announced on the changelog before they ship.'
termsOfService: https://ci-hub.com/legal/terms
contact:
name: CI HUB GmbH
url: https://developer.ci-hub.com/access
externalDocs:
description: CI HUB Access SDK reference
url: https://developer.ci-hub.com/access
servers:
- url: https://live.ci-hub.com/api/v1
description: Production
- url: https://stage.ci-hub.com/api/v1
description: Staging / integration environment used throughout the published examples
security:
- cihubAccessToken: []
paths:
/auth/exchangeToken:
post:
operationId: exchangeToken
summary: Exchange token
description: 'Exchanges a partner-signed JWT for CI HUB access and refresh tokens. Required as the
first call of every Access SDK session.
## Partner JWT
Send the partner-signed JWT as `Authorization: Bearer <partner-signed-jwt>`.
### Header
| Field | Required | Value |
|---|---|---|
| `alg` | yes | `RS256` |
| `kid` | yes | Must match a key published in the partner JWKS |
| `typ` | optional | `JWT` |
`HS256` is not accepted.
### Payload
| Claim | Type | Required | Notes |
|---|---|---|---|
| `iss` | string | yes | Partner issuer URL. Must match the registered value exactly, including
trailing slash. |
| `aud` | string | yes | Registered audience. Default `https://api.ci-hub.com`. |
| `sub` | string | yes | Stable identifier for the user in the partner system. |
| `iat` | number | yes | Unix seconds. Tolerance: up to 30 seconds in the future. The token is
rejected once it is older than `maxTokenAge` (now minus `iat`). |
| `exp` | number | yes | Unix seconds. Must be in the future. |
| `email` | string | yes (in JWT or body) | Used for just-in-time (JIT) user resolution: CI HUB
finds the matching user or creates one on first exchange. Must parse as an email. JWT value takes
precedence over body. |
| `given_name` | string | optional | First name. Falls back to splitting `name`. |
| `family_name` | string | optional | Last name. Falls back to splitting `name`. |
| `name` | string | optional | Display name. Used when `given_name` and `family_name` are absent.
|
The token must be no older than `maxTokenAge` seconds (default 3600), measured from
`iat` to the current time. Sign a fresh JWT for each exchange. Stale tokens are
rejected with `cihub-sdk-token-invalid`.
Send `Content-Type: application/json` so the JSON body parser picks up the request.
Only an `application/json` body is parsed for the `email` field; other content types
leave it unread. Name fields are not read from the body. They come from the partner
JWT claims (`given_name`, `family_name`, `name`).'
requestBody:
description: 'Optional. Used only as a fallback for partners that cannot include the `email`
claim in the partner JWT. If the JWT contains `email`, the body value is ignored.
The request must include `email` somewhere (JWT or body). If your JWT already
carries `email`, send `{}`.'
content:
application/json:
schema:
type: object
properties:
email:
type: string
description: Fallback email for JIT user resolution when the JWT has no `email` claim.
format: email
examples:
- jane@customer.example.com
responses:
'200':
description: '@description Token exchange successful.'
content:
application/json:
schema:
type: object
properties:
access_token:
type: string
description: 'CI HUB access token. Sent on subsequent calls as `Authorization: Bearer
<access_token>`. Valid for 1 hour.'
examples:
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
refresh_token:
type: string
description: CI HUB refresh token. 30-day lifetime. Used to mint new access tokens.
examples:
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
expires_in:
type: number
description: Access token lifetime in seconds. Always 3600.
examples:
- 3600
token_type:
type: string
const: Bearer
description: Always `Bearer`.
required:
- access_token
- refresh_token
- expires_in
- token_type
'400':
description: '`cihub-sdk-email-missing`: no `email` claim and no body fallback, or the value
fails format validation.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: '| Code | When |
|---|---|
| `cihub-sdk-token-missing` | No `Authorization` header. |
| `cihub-sdk-token-invalid` | Malformed JWT, wrong algorithm, missing required field, signature
failed, `iat` more than 30s in the future, token older than `maxTokenAge` (now minus `iat`).
|
| `cihub-sdk-token-expired` | JWT past `exp`. |
`cihub-sdk-token-invalid` is rarely also returned when CI HUB fails to persist
the user during the exchange (transient backend fault). If a known-good JWT
suddenly fails, retry once before treating the token as the problem.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: '`cihub-sdk-no-subscription`: the partner company has no active SDK subscription.
Contact CI HUB before retrying.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: '| Code | When |
|---|---|
| `cihub-sdk-partner-unknown` | `iss` claim not registered as an SDK partner. |
| `cihub-sdk-audience-invalid` | `aud` claim does not match the registered audience. |'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
description: '`cihub-rate-limited`: the partner exceeded its exchange rate limit. Standard
`RateLimit-*` headers describe the window; back off and retry after it resets.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
security:
- cihubAccessToken: []
/auth/checkToken:
get:
operationId: checkTokenSdk
summary: Check token
description: 'Verifies that a CI HUB access token is still valid and returns the user profile
envelope the partner platform needs to display in its UI. Typical use: confirm an
active session before rendering a CI HUB-backed view, or revalidate after a long
idle period.
`licenseState`, `licenseExpires`, and `isTrialLicense` describe the SDK
subscription, not the DAM provider. They are informational; the SDK subscription
itself is re-checked on every call.
A 401 here is the cue to start a new exchange. A 402 means the subscription state
changed: contact CI HUB before retrying.'
responses:
'200':
description: '@description Token is valid.'
content:
application/json:
schema:
type: object
properties:
adapter:
type: string
description: Always "CI HUB".
examples:
- CI HUB
source:
type: string
description: CI HUB host that served the response.
examples:
- api.ci-hub.com
user:
type: string
description: Display name composed from the user record.
examples:
- Jane Doe
account:
type: string
description: Email address tied to the user record.
examples:
- jane@customer.example.com
licenseState:
type: string
description: Always "Subscription" for SDK partners.
examples:
- Subscription
licenseExpires:
type: number
description: 'Unix milliseconds. End of the active SDK subscription''s last day in
UTC.
Perpetual subscriptions emit a far-future sentinel; treat any value greater
than `Date.now()` as valid.'
examples:
- 1798761599999
isTrialLicense:
type: boolean
description: Always false for SDK partners.
examples:
- false
userHash:
type: string
description: Stable, opaque per-user identifier suitable for partner-side analytics.
examples:
- a1b2c3d4e5f6g7h8i9j0k1l2
'401':
description: '| Code | When |
|---|---|
| `cihub-access-token-missing` | No `Authorization` header. |
| `cihub-access-token-invalid` | Token signature failed, token expired, or the user record
was removed. |'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: '@description `cihub-sdk-no-subscription`: the partner''s SDK subscription is no
longer active.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: '`cihub-sdk-partner-unknown`: the partner registration was removed since the
access token was issued.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
description: '@description `cihub-internal-error`: the subscription re-check failed transiently.
Safe to retry.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
security:
- cihubAccessToken: []
/auth/refreshToken:
get:
operationId: refreshTokenSdk
summary: Refresh token
description: 'Renews a session. This is a shared endpoint: the token in `provider-authorization`
decides which session is renewed.
## CI HUB session
Send the CI HUB refresh token returned at exchange in `provider-authorization` to
mint a new access token and a new refresh token. `Authorization` carries the current
access token, which may already be expired (only its signature is checked here).
The refresh token''s `sub` must match the access token''s `sub`; cross-user refresh
attempts are rejected.
The new access token is valid for 1 hour, the new refresh token for 30 days.
Replace both cached tokens with the values returned here. The previous access token
is superseded and should be discarded by the client, but stays valid until its
`exp`; the previous refresh token remains valid until its 30-day clock runs out, so
a slow client switch-over is safe.
Refresh proactively a few minutes before `expires_in`, or reactively after
receiving `cihub-access-token-invalid` from any endpoint. Once a refresh token
expires the partner must perform a new exchange.
## DAM connection
Send the DAM `refresh_token` from the login poll in `provider-authorization` to
renew a DAM connection token. The token also identifies the provider. Some
providers only return a new `access_token`; in that case keep the prior
`refresh_token` and reuse it on the next refresh. A provider with no refresh path
returns 404: run a fresh DAM login. Handle every provider the same way: try to
refresh, and fall back to a fresh login if the refresh fails.
The CI HUB SDK subscription is re-checked on every refresh; partners whose
subscription lapsed receive 402 here and must contact CI HUB before continuing.'
responses:
'200':
description: '@description Token refreshed successfully.'
content:
application/json:
schema:
type: object
properties:
access_token:
type: string
description: New access token (CI HUB session) or new DAM connection token, matching
the refreshed session.
examples:
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
refresh_token:
type: string
description: New refresh token. Some DAM providers omit it; keep and reuse the prior
refresh token then.
examples:
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
expires_in:
type: number
description: Access token lifetime in seconds. Omitted when the DAM provider does
not supply a token lifetime.
examples:
- 3600
token_type:
type: string
description: Token type for the Authorization header.
examples:
- Bearer
required:
- access_token
- token_type
'401':
description: '| Code | When |
|---|---|
| `cihub-access-token-missing` | No `Authorization` header. |
| `cihub-access-token-invalid` | Access token signature failed or was malformed. An expired
access token is accepted here; only signature and format are checked. |
| `cihub-refresh-token-invalid` | The `provider-authorization` token is not a refresh token
(for example an access token sent in its place), or its `sub` does not match the access token''s
`sub`. |
A `cihub-refresh-token-invalid`, and any failure once the 30-day refresh window
has lapsed, is the cue to start a new exchange.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: '@description `cihub-sdk-no-subscription`: the partner''s SDK subscription is no
longer active.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: '| Code | When |
|---|---|
| `provider-access-token-missing` | No `provider-authorization` header. |
| `provider-access-token-invalid` | Refresh token signature failed, was malformed, or has
expired. |
| `cihub-sdk-partner-unknown` | The partner registration was removed since the tokens were
issued. |'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: 'The DAM provider has no refresh path. No structured `error` envelope. Run a
fresh DAM login.'
'503':
description: '@description `cihub-internal-error`: the subscription re-check failed transiently.
Safe to retry.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
security:
- cihubAccessToken: []
damToken: []
/auth/logout:
get:
operationId: logoutSdk
summary: Logout
description: 'Signals the end of a partner-side session. For Access SDK partners this endpoint is
advisory: CI HUB does not maintain server-side session state for SDK tokens, so
logout returns 200 without revoking the refresh token. The partner platform is
responsible for discarding the cached access and refresh tokens locally.
CI HUB does not currently maintain a refresh-token blocklist for Access SDK
partners. A refresh token remains valid until its 30-day clock runs out, even after
a logout call. Partners that need stronger revocation guarantees should:
- Drop both the access token and the refresh token from local storage on logout.
- Not persist refresh tokens beyond the active session.
- Detect compromise on the partner side and avoid reusing a leaked refresh token.
Server-side revocation is in progress.'
responses:
'200':
description: '@description Logout acknowledged. The response body is the literal string `OK`.'
'401':
description: '| Code | When |
|---|---|
| `cihub-access-token-missing` | No `Authorization` header. |
| `cihub-access-token-invalid` | Access token signature failed or was malformed. |
A 401 here is harmless from a logout perspective: the token was not valid to
begin with. The partner should still discard local copies.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
security:
- cihubAccessToken: []
/auth/providers:
get:
operationId: getProvidersSdk
summary: Get providers
description: 'Returns the list of DAM providers available to the authenticated CI HUB user. The
partner platform uses the result to render a connection picker and to pass each
provider''s `id` on subsequent login and content calls.
This endpoint is intentionally lenient on authentication so the partner platform
can render its connection picker before any DAM session exists. A missing or
invalid `Authorization` header returns 200 with a single-entry list containing
only the CI HUB platform connection. When the list contains only `cihub`, refresh
the access token or start a new exchange.
The provider list is stable for the lifetime of one access token. Cache it for
that long and refetch after a new exchange.'
responses:
'200':
description: '@description List of available providers'
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
description: '@description Provider identifier'
name:
type: string
description: '@description Provider display name'
version:
type: string
description: '@description Provider version'
logo:
type: object
properties:
data:
type: string
description: Base64 encoded image data
examples:
- data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
width:
type: number
description: Logo width in pixels
examples:
- 249
height:
type: number
description: Logo height in pixels
examples:
- 77
backgroundColor:
type: string
description: Background color in hex format
examples:
- '#FFFFFF'
description: '@description Provider logo with metadata.'
glyph:
type: object
properties:
data:
type: string
description: Base64 encoded image data
examples:
- data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
width:
type: number
description: Glyph width in pixels
examples:
- 249
height:
type: number
description: Glyph height in pixels
examples:
- 77
backgroundColor:
type: string
description: Background color in hex format
examples:
- '#FFFFFF'
description: '@description Provider glyph/icon with metadata'
capabilities:
$ref: '#/components/schemas/ProviderCapabilities'
loginUrl:
type: string
description: '@description Login URL for this provider. POST it with `?polling=true`
to run the JSON polling login flow.'
isDefault:
type: boolean
description: '@description Whether this is a default provider'
isOptional:
type: boolean
description: '@description Whether this is an optional provider'
isLicensed:
type: boolean
description: '@description Whether the provider requires a paid tier. This does
not indicate whether the current end user holds that license.'
isUnsupported:
type: boolean
description: Present with value `true` only when the provider is wired but no longer
maintained. Absent otherwise; `false` is never sent.
examples:
- true
unsupportedAdapterTitle:
type: string
description: Title to display when the provider is unsupported. May be absent even
when `isUnsupported` is `true`; null-guard before reading.
examples:
- Ask your DAM Vendor
unsupportedAdapterDescription:
type: string
description: Description to display when the provider is unsupported. May be absent
even when `isUnsupported` is `true`; null-guard before reading.
examples:
- This Solution is not supported today, please ask the DAM Vendor for a status.
isDisabledForHost:
type: boolean
description: '@description Whether this provider is disabled for the current host.'
'500':
description: '@description Transient backend failure. Safe to retry.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
security:
- cihubAccessToken: []
/auth/login:
get:
operationId: damLoginPoll
summary: DAM login (poll)
description: 'Returns the current state of a login started by initiate. The `state` value is the
credential for this call, so no `Authorization` header is needed. Poll on an
interval until the response carries tokens or an error.
The returned `access_token` is the DAM connection token and is separate from the
CI HUB access token in your `Authorization` header. Content calls carry both: the
CI HUB token in `Authorization` and this token in `provider-authorization`.
A successful poll consumes the `state`. A `state` lives for 5 minutes from
initiate; once it expires (or after a successful poll), further polls take the
failure path. Cap your loop at the 5-minute horizon and fall back to a fresh login
on failure.
The poll always answers 200 with a JSON body. An unknown, expired, or consumed
`state`, and a login the end user failed or canceled at the DAM, all return a body
carrying an `error` property instead of tokens. Treat any body with `error` as
final and start a new DAM login.'
parameters:
- name: state
in: query
required: true
description: '@description The `state` returned by initiate. Identifies and authorizes the call.'
schema:
type: string
- name: polling
in: query
required: true
description: '@description Send `true` on every initiate and poll call; it selects the JSON response
shape.'
schema:
type: boolean
const: true
responses:
'200':
description: 'While the state is alive and the user has not finished signing in, the poll
returns an empty object. Once the login completes, it returns the tokens. A
dead `state` (unknown, expired, consumed, or failed login) returns an `error`
property.'
content:
application/json:
schema:
anyOf:
- type: object
- type: object
properties:
access_token:
type: string
description: '@description DAM connection token. Send it as the `provider-authorization`
header on content calls.'
refresh_token:
type: string
description: '@description DAM refresh token. Use it to renew the connection.'
userHash:
type: string
description: '@description Stable, opaque per-user identifier for the connected
DAM account. Optional for partners to use.'
required:
- access_token
- refresh_token
- type: object
properties:
error:
type: string
description: Plain-text failure marker.
examples:
- Not found
required:
- error
security:
- cihubAccessToken: []
post:
operationId: damLoginInitiate
summary: DAM login (initiate)
description: 'Starts a login for the provider named in the `provider` query parameter (an `id`
from the providers listing). DAM login is a two-call flow: the partner platform
starts a login for a chosen provider, opens the returned URL in the end user''s
browser, then polls until the end user finishes signing in at the DAM.
Open `redirect_uri` for the end user (popup or full-page redirect). Keep `state`
for polling.
Failures past the token check (an unknown provider, a provider the user is not
licensed for, or a rejected sign-in) have not moved to the error envelope yet. They
currently redirect the browser to a CI HUB URL carrying an `error` query parameter
with a plain-text message. Treat a redirect or non-JSON response from initiate as a
failure and read the `error` value.
## Provider parameters
Several providers are multi-tenant, so CI HUB asks the end user which instance of the
DAM to sign in to on a page of its own before the provider''s login. A partner platform
that already knows the instance sends it as `serverUrl` on the initiate call, and that
page drops out of the flow. `bynder`, `dash`, `fotoware`, `frontify`, `picturepark`,
and `purered` read it.
It is optional. Omit it and the end user answers the prompt as before. A value the
provider rejects also falls back to the prompt, so a stale instance URL degrades
instead of failing the login.'
parameters:
- name: provider
in: query
required: true
description: '@description Provider identifier from the providers listing.'
schema:
type: string
- name: polling
in: query
required: true
description: '@description Send `true` on every initiate and poll call; it selects the JSON response
shape.'
schema:
type: boolean
const: true
- name: serverUrl
in: query
description: 'Provider parameter. The DAM instance the end user signs in to, as a full origin.
Read by `bynder`, `dash`, `fotoware`, `frontify`, `picturepark`, and `purered`.'
schema:
type: string
responses:
'200':
description: '@description Login initiated.'
content:
application/json:
schema:
type: object
properties:
redirect_uri:
type: string
description: One-time URL to open in the end user's browser so they can authenticate
at the DAM.
examples:
- https://provider.example.com/oauth/authorize?...&state=Pf3a9c...
state:
type: string
description: Opaque token that identifies this login. Pass it to the poll call.
examples:
- Pf3a9c...
required:
- redirect_uri
- state
'302':
description: 'Legacy failure path: an unknown provider, a provider the user is not licensed
# --- truncated at 32 KB (102 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/ci-hub/refs/heads/main/openapi/ci-hub-access-openapi.yml