Skip to content

Authentication

The Zandovi API authenticates every request with an API key sent in the X-Api-Key header, or with an OAuth 2.1 access token sent as Authorization: Bearer. Either credential is scoped to a single workspace (organization) and inherits that workspace’s plan and quota.

  1. Sign in to the app and open Settings → API Keys.
  2. Enter a Key name (e.g. Production, Newsletter Service) and click Create key.
  3. Copy the key from the “Key created — copy it now” banner. It is shown only once and never again.

See API Keys for the full in-app walkthrough, including the 5-key limit and per-key usage.

Add the X-Api-Key header to every request:

X-Api-Key: your_api_key

Example with curl:

Terminal window
curl https://app.zandovi.com/api/v1/projects \
-H "X-Api-Key: $ZANDOVI_API_KEY"

A missing or invalid key returns 401 Unauthorized with an application/problem+json body.

  • A key can only access templates and projects in its own workspace.
  • Across a workspace, a key can read its owner’s private and shared templates, plus templates shared by other members. Private templates belonging to other members are not accessible.
  • Every key carries a set of scopes that decide which endpoints it may call. You pick them with the scope checkboxes when you create the key in Settings → API Keys; leaving them untouched gives the default design:read + templates:read + render (validate designs, list and get templates and projects, render images, batch jobs, share links). Keys created before scopes existed have that same default (design:read was added to every existing key when design validation shipped). Tick templates:write for a key that creates, updates or deletes templates and projects. Calling an endpoint the key is not scoped for returns 403 INSUFFICIENT_SCOPE. The scopes table below lists every scope and what it grants; the same vocabulary applies to API keys and OAuth tokens.

Open Settings → API Keys, click the delete (trash) icon next to a key, and confirm. Revoked keys stop working immediately. Rotate keys by creating a new one, updating your integration, then deleting the old key.

Each key shares its workspace’s monthly render quota. Generation responses include X-Quota-Limit, X-Quota-Remaining, and X-Quota-Reset headers so you can track consumption — see Errors, quotas & rate limits.

If you are building an integration that other Zandovi users will connect to their own workspace (an MCP client, a third-party app), authenticate with OAuth 2.1 instead of asking users for an API key. The public API accepts an OAuth access token the same way it accepts an API key: send it as Authorization: Bearer <token> instead of X-Api-Key. Never send both headers on the same request; the API rejects that as 400 AMBIGUOUS_CREDENTIALS.

This section is for people writing a connector. If you are a Zandovi user connecting an existing AI assistant, see Claude.ai & Claude Desktop, ChatGPT or Claude Code & coding agents instead.

A token is issued with one or more scopes. The client names the scopes it wants in the authorization request’s scope parameter; the consent page offers exactly those, all ticked, and the user can untick any of them but never add one. A request that names no scope (an absent or blank scope, or only account:read) is offered every grantable scope, so a connector that doesn’t list scopes can still be granted write access; an unknown scope name is refused with invalid_scope. API keys use the same scopes, chosen at key creation:

ScopeGrants
account:readGET /api/v1/account. Implicit on every token; never requested or granted explicitly.
design:readValidate a design without saving it: POST /api/v1/designs/validate; the Design Reference family (GET /api/v1/design-reference*). On the MCP server, validate_design (the knowledge tools get_design_reference and search_icons need no scope). Part of the default set.
templates:readList and get templates and projects, including GET /api/v1/templates/{id}?include=canvas. Part of the default set.
renderAnything that renders: single generate, batch jobs, share links, and the charged design preview POST /api/v1/designs/render. Part of the default set.
templates:writeCreate, update and delete templates (POST /api/v1/templates, PATCH and DELETE /api/v1/templates/{id}) and create projects (POST /api/v1/projects). See Authoring Templates.
images:readList and preview the images uploaded to the organization: GET /api/v1/images, GET /api/v1/images/{id}, GET /api/v1/images/{id}/preview. See Image Library.
images:writeUpload and delete images (POST/DELETE /api/v1/images). See Image Library. On the MCP server, also gates search_stock_images; the public API itself has no stock-search endpoint — see MCP Tools.

Calling an endpoint without the scope it requires returns 403 INSUFFICIENT_SCOPE. images:read and images:write are independent: a credential with only images:write can upload but not list, and vice versa.

A connection holds the scopes approved at consent; it never gains one later on its own. To change what a connection may do, reconnect from the assistant: the consent page again ticks every scope the client requests (every grantable scope when it requests none), and you untick what it should not get. Settings → Connected apps shows what each connection holds.

Zandovi is an OAuth 2.1 authorization server. A compliant client can discover everything it needs from these two well-known documents; you should not hard-code the endpoint URLs below.

  • Authorization server metadata (RFC 8414): GET https://app.zandovi.com/.well-known/oauth-authorization-server. Lists the authorization, token, registration and revocation endpoints, the supported scopes, grant types, PKCE methods and client authentication methods, and client_id_metadata_document_supported: true. The issuer is https://app.zandovi.com, the same origin that serves the API.
  • Protected resource metadata (RFC 9728), for MCP clients: GET https://mcp.zandovi.com/.well-known/oauth-protected-resource. Points at the authorization server above.

All four live on the app origin, next to the API:

EndpointPurpose
GET https://app.zandovi.com/oauth/authorizeStarts the authorization-code flow. Redirects the user to app.zandovi.com/connect to sign in, pick an organization and approve scopes.
POST https://app.zandovi.com/oauth/tokenExchanges an authorization code (with PKCE verifier) for tokens, or a refresh token for a new access token.
POST https://app.zandovi.com/oauth/registerDynamic client registration (RFC 7591). Open to any client; rate-limited to 10 registrations per IP address per hour.
POST https://app.zandovi.com/oauth/revokeRevokes a token.

PKCE with S256 is mandatory on every authorization request; plain is not accepted. Supported client authentication methods at the token endpoint are none (public clients), client_secret_basic, client_secret_post and private_key_jwt (signed client assertions, see Client authentication methods below). Access tokens are valid for 1 hour; refresh tokens are valid for 30 days and rotate on use (each refresh call invalidates the previous refresh token and issues a new one). A request with an expired, unknown or revoked bearer token gets a 401 with WWW-Authenticate: Bearer error="invalid_token", so a well-behaved client knows to refresh or restart the flow.

Zandovi supports two ways for a client to identify itself, so integrators can pick whichever their platform’s MCP guidance recommends:

  • Dynamic Client Registration (DCR), RFC 7591: POST /oauth/register with the client’s metadata (name, redirect URIs). Zandovi returns a client_id (and, for a confidential client, a secret) that the client stores and reuses on every subsequent authorization. This is what Claude.ai uses, and what you get if you register manually and paste the resulting client ID into a connector’s advanced settings. A DCR registration can also request private_key_jwt instead of a secret:

    POST /oauth/register
    {
    "client_name": "My Connector",
    "redirect_uris": ["https://example.com/callback"],
    "token_endpoint_auth_method": "private_key_jwt",
    "jwks_uri": "https://example.com/.well-known/jwks.json"
    }

    Zandovi issues no client_secret for this method; it stores and echoes back the jwks_uri instead. An inline jwks body is refused, and jwks_uri is refused with any method other than private_key_jwt.

  • Client ID Metadata Documents (CIMD), the approach OpenAI recommends for ChatGPT connectors: the client’s client_id is itself an https:// URL pointing at a small JSON document describing the client (name, redirect URIs, token endpoint auth method). Zandovi negotiates the client’s authentication method from that document instead of requiring it to declare none: it reads token_endpoint_auth_methods_supported when the document lists one, otherwise the single token_endpoint_auth_method (default none), and picks whichever of those methods Zandovi accepts from a metadata-document client, none or private_key_jwt (the preferred method wins when Zandovi accepts it, otherwise the first accepted entry in the document’s own order). A document that negotiates to neither is refused. A document negotiating to private_key_jwt must also declare an https jwks_uri. Zandovi fetches and caches that document the first time it sees the URL, instead of requiring an upfront registration call. There is nothing to register ahead of time.

Both mechanisms produce an OAuthClient Zandovi treats identically from then on: same scopes, same token lifetimes, same revocation. Pick DCR if your platform expects to call a registration endpoint and store the resulting credentials; pick CIMD if your platform expects to publish its own client metadata at a stable URL.

Every registered or negotiated client is held to the one token_endpoint_auth_method it registered with, for both CIMD and DCR clients. The token endpoint (and POST /oauth/revoke) accepts:

  • none: a public client authenticates with client_id alone; PKCE carries the proof. It cannot later present a client assertion.

  • client_secret_basic / client_secret_post: a confidential client sends its client_secret as an Authorization: Basic header or as a form field.

  • private_key_jwt (RFC 7523): the client signs a JWT assertion with a private key instead of holding a secret. It must have registered (or, for CIMD, negotiated) an https jwks_uri publishing the public key(s); Zandovi never accepts an inline jwks, and a CIMD document that declares token_endpoint_auth_signing_alg must declare RS256. On each request the client sends client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer and a client_assertion that is:

    • signed with RS256 by a key published at the client’s jwks_uri;
    • iss and sub equal to the client’s client_id;
    • aud equal to the token endpoint (https://app.zandovi.com/oauth/token) or the bare issuer (https://app.zandovi.com), as a string or array;
    • no more than 5 minutes old at exp (the exp - iat lifetime is capped, on top of a 60-second clock-skew allowance);
    • carrying a jti that is single-use: a second assertion with the same jti is refused.

    The same assertion shape authenticates grant_type=refresh_token requests and POST /oauth/revoke, not just the initial token exchange.

A client authenticates only with its registered method: a private_key_jwt client cannot fall back to client_id plus PKCE, and a none or secret client cannot present an assertion. A failed client authentication is 401 invalid_client with a generic error description (the response never says which check failed); a malformed request, such as an assertion sent together with a secret or a missing client_assertion_type, is 400 invalid_request.

A registered redirect URI must be an absolute URI with no fragment, and one of: https:// with a host; http:// on a loopback host (localhost, 127.0.0.1, [::1]) for native apps; or a private-use scheme in reverse-domain form (e.g. com.example.app:/callback) per RFC 8252. javascript:, data: and similar script-executing schemes are always refused.

An OAuth token behaves exactly like an API key for quota purposes: every render it makes charges the organization’s API render quota, including on plans with unlimited renders in the designer. A token is bound to the single organization the user picked at consent; to connect a different organization, the user disconnects (Settings → Connected apps) and connects again.