Skip to content
Last updated

Authentication

The auth object tells the platform how users connect their accounts with your service.

Bearer Token (API Key)

The simplest auth method. The user pastes their API key into a form, and the platform stores it securely.

auth: {
  type: 'bearer_token',

  // Keys that must NEVER be sent to the frontend
  sensitiveKeys: ['accessToken'],

  // Default credential values (empty = user must provide)
  config: {
    accessToken: '',
  },

  // Verify the token works by calling a "who am I" endpoint
  userDetails: {
    url: 'https://api.example.com/users/me',
    method: 'GET',
    headers: {
      Authorization: 'Bearer [[accessToken]]',
      'Content-Type': 'application/json',
    },
    bodyType: 'json',
    body: {},
    mapping: {
      uid: '$.user.id',        // Unique user identifier → metadata
      name: '$.user.name',     // Display name → metadata
    },
  },
}

How it works:

  1. User enters their API key in the settings form
  2. Platform calls saveMiniAppCredentials mutation
  3. If get_token is defined, it's executed to exchange/validate credentials
  4. Tokens and secrets are stored in InstalledMiniApps.credentials
  5. If userDetails is defined, mapped store/account fields are stored in InstalledMiniApps.metadata
  6. Status set to connected

Per-tenant install storage

Each installed miniapp persists three server-side bags on InstalledMiniApps:

FieldContents[[key]]{{key}}Exposed to UI?
credentialsTokens, secrets, registration outputs (accessToken, webhookId, …)YesNoNever
metadataFull userDetails.mapping result (storeId, storeName, uid, …)YesYes (runtime)Never
userInputUser-entered install form valuesNoYesPre-fill only

[[key]] resolves from credentials first, then metadata (credentials win on key collision). Use camelCase keys in mappings (e.g. storeId) even when the HTTP header name differs (Store-Id: '[[storeId]]').

OAuth 2.0

For services that require OAuth 2.0 authorization (Salla, Google, etc.).

auth: {
  type: 'oauth2',

  sensitiveKeys: ['accessToken', 'refreshToken'],

  // OAuth configuration (available as {{key}} placeholders)
  config: {
    client_id: 'your-client-id',
    client_secret: 'your-client-secret',
    scope: 'read write',
    response_type: 'code',
    grant_type: 'authorization_code',
    accessToken: '',
    refreshToken: '',
  },

  // Build the authorization redirect URL
  auth_url: {
    url: 'https://provider.com/oauth/authorize?client_id={{client_id}}&scope={{scope}}&response_type={{response_type}}&redirect_uri={{redirect_uri}}',
    method: 'GET',
    headers: {},
    bodyType: 'json',
    body: {},
    mapping: {},
  },

  // Exchange authorization code for tokens
  get_token: {
    url: 'https://provider.com/oauth/token',
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    bodyType: 'json',
    body: {
      client_id: '{{client_id}}',
      client_secret: '{{client_secret}}',
      grant_type: '{{grant_type}}',
      code: '{{code}}',
      redirect_uri: '{{redirect_uri}}',
    },
    // Map response to credential keys
    mapping: {
      accessToken: '$.access_token',
      refreshToken: '$.refresh_token',
      expiresIn: '$.expires_in',
    },
  },

  // Refresh expired tokens
  refresh_token: {
    url: 'https://provider.com/oauth/token',
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    bodyType: 'json',
    body: {
      client_id: '{{client_id}}',
      client_secret: '{{client_secret}}',
      grant_type: 'refresh_token',
      refresh_token: '[[refreshToken]]',
    },
    mapping: {
      accessToken: '$.access_token',
      refreshToken: '$.refresh_token',
      expiresIn: '$.expires_in',
    },
  },

  // Auto-refresh on 401 responses
  auto_refresh: true,

  // Fetch user identity after authentication
  userDetails: {
    url: 'https://api.provider.com/v2/oauth2/user/info',
    method: 'GET',
    headers: {
      Authorization: 'Bearer [[accessToken]]',
      'Content-Type': 'application/json',
    },
    bodyType: 'json',
    body: {},
    mapping: {
      uid: '$.data.id',
      name: '$.data.name',
    },
  },

  // Post-auth setup (see §4.4)
  registrationRequests: [],
}

OAuth 2.0 Flow:

User clicks "Connect" in Marketplace


┌─────────────────────┐
│ Browser popup opens  │
│ GET /:appNs/oauth2   │
└────────┬────────────┘


┌─────────────────────┐
│ Platform builds      │
│ auth_url & redirects │
│ to provider          │
└────────┬────────────┘
         │ User authorizes

┌─────────────────────┐
│ Provider redirects   │
│ back with ?code=xxx  │
└────────┬────────────┘


┌─────────────────────┐
│ Platform executes    │
│ get_token request    │
│ (code → tokens)      │
└────────┬────────────┘


┌─────────────────────┐
│ Platform executes    │
│ userDetails request  │
│ (extract uid, name)  │
└────────┬────────────┘


┌─────────────────────┐
│ Tokens stored in     │
│ credentials;         │
│ userDetails → metadata │
│ registrationRequests   │
│ executed (if any)      │
└────────┬────────────┘


┌─────────────────────┐
│ Popup closes, main   │
│ window refreshes     │
└─────────────────────┘

Token Refresh

When auto_refresh: true is set and any action request returns a 401 response:

  1. Platform automatically calls the refresh_token request
  2. New credentials are extracted via the mapping
  3. Credentials are updated in the database
  4. The original failed request is retried with the new token

This is handled transparently — no user interaction required.

Registration Requests (Post-Auth Setup)

Registration requests are API calls executed once right after a user successfully authenticates. Use them to:

  • Register webhooks with the provider
  • Fetch initial configuration (store ID, workspace info)
  • Subscribe to events
auth: {
  // ...auth config...

  registrationRequests: [
    // Register your webhook URL with the provider
    {
      url: 'https://api.provider.com/webhooks',
      method: 'POST',
      headers: {
        Authorization: 'Bearer [[accessToken]]',
        'Content-Type': 'application/json',
      },
      bodyType: 'json',
      body: {
        url: '{{webhookUrl}}',     // Auto-injected: your webhook endpoint
        events: ['order.created', 'customer.updated'],
        secret: '{{webhookSecret}}',
      },
      mapping: {
        webhookId: '$.id',  // Saved to credentials for later reference
      },
    },

    // Prefer auth.userDetails for store profile; use registrationRequests
    // only for one-off setup outputs like webhook IDs.
    {
      url: 'https://api.provider.com/webhooks/subscribe',
      method: 'POST',
      headers: {
        Authorization: 'Bearer [[accessToken]]',
        'Store-Id': '[[storeId]]',   // Resolved from metadata
      },
      bodyType: 'json',
      body: {
        url: '{{webhookUrl}}',
      },
      mapping: {
        webhookSubscriptionId: '$.id',
      },
    },
  ],
}

Available placeholders in registration requests:

PlaceholderSourceDescription
[[accessToken]]Stored credentialsThe OAuth access token
[[refreshToken]]Stored credentialsThe OAuth refresh token
[[storeId]]Stored metadataPlatform context from userDetails (e.g. Zid)
[[any_key]]credentials + metadataAny key from either bag (credentials win)
{{webhookUrl}}System-generatedYour miniapp's webhook endpoint URL
{{subdomain}}System contextThe tenant's subdomain
{{uid}}metadata / top-level uidThe authenticated user's ID
{{configKey}}auth.configAny key from the auth config object

Important: Object mappings in registrationRequests are persisted to credentials. userDetails mappings are persisted to metadata in full. Both bags are available for [[key]] placeholders in sync headers, actions, and automations.