Extensions

Installing Extensions

An install pins a specific published version of an extension to an application, mints a dedicated API key (and optionally a webhook signing secret), and begins event delivery if a webhookUrl was declared.

Install an extension

Pass the versionId of the published version you want to install. Optionally pass config — a free-form JSON object that your extension reads at runtime. The install's environment is derived from the caller's API key or dashboard-selected X-Throttle-Environment-Id and cannot be overridden by the request body.

Install
# Install a published version of an extension into an application.
# Both the dashboard session token (Clerk JWT) and the Application ID header
# are required — installs are application-scoped.
curl -X POST https://api.usethrottle.dev/api/v1/extensions/ext_xxx/install \
  -H "Authorization: Bearer <dashboard-session>" \
  -H "X-Throttle-Application-Id: <applicationId>" \
  -H "X-Throttle-Environment-Id: <workspace-environment-uuid>" \
  -H "content-type: application/json" \
  -d '{
    "versionId": "ver_xxx",
    "config": {
      "syncInterval": 60,
      "notifyOnNew": true
    }
  }'
# ─── Response (apiKey is shown once — store it immediately) ──────────────────
{
  "data": {
    "id":          "inst_01HZX...",
    "extensionId": "ext_xxx",
    "versionId":   "ver_xxx",
    "status":      "active",
    "environmentId": "8a5f2d1e-3d2a-4d24-93a1-fd3f9e1f37f0",
    "environmentSlug": "uat",
    "environmentKind": "non_production",
    "providerEnvironment": "sandbox",
    "config":      { "syncInterval": 60, "notifyOnNew": true },
    "installSequence": 1,
    "createdAt":   "2026-05-31T10:00:00.000Z",
    "apiKey":               "sk_uat_...",        // ← never shown again
    "webhookSigningSecret": "whsec_..."           // ← only on first install; read it back any time
  }
}
One-time secrets
apiKey is shown exactly once in the install response. If you lose it, the only recovery is to uninstall and reinstall — a new key is minted. The webhookSigningSecret is different: it is returned once on first install, but it is stable and your extension can read it back at any time (see below), so losing it is recoverable without a reinstall.

Read your signing secret

Your extension can fetch the signing secret for its own installation, so it never has to depend on a human copying the value out of the dashboard at install time:

Read the signing secret
# Your extension reads the signing secret for its own installation.
# Authenticate with the installation's own API key.
curl https://api.usethrottle.dev/api/v1/installations/inst_01HZX.../webhook-secret \
  -H "X-API-Key: sk_uat_..."

# ─── Response ─────────────────────────────────────────────────────────
{
  "data": {
    "installationId":  "inst_01HZX...",
    "installSequence": 2,
    "endpointId":      "we_01HZY...",
    "url":             "https://your-extension.example/webhooks/throttle",
    "enabledEvents":   ["order.created"],
    "signingSecret":   "whsec_..."
  }
}

Authenticate with the installation's own API key (or its extension session token). A dashboard user holding application:extensions:install can also read it. Any other credential gets a 404. The route needs no particular scope — the credential itself is the authorisation.

The secret survives a reinstall
Uninstalling and reinstalling reuses the same installation row, and deliberately keeps the signing secret so verification does not silently start failing. Use installSequence — which increments on every (re)install while id stays put — to tell a reinstall from a restart, and to know when to discard credentials you cached against the install.

Scope and event consent

When you install an extension, you implicitly consent to its declared scopes and event subscriptions. The installer must hold the extensions:install scope (API key) or the application:extensions:install RBAC permission (dashboard user). The issued API key is scoped exactly to the version's declared scopes.

If the extension later publishes a new version with additional scopes, upgrading to that version requires re-consent. The upgrade endpoint returns 409 new_scopes_require_consent listing the new scopes, and the caller must retry with acknowledgedNewScopes: true. See the Versioning guide .

Configuration

The config field is a free-form JSON object passed at install time and updateable any time thereafter. Use it for extension-specific settings such as API keys for third-party services, feature flags, or integration identifiers. Your extension reads it from the installation record; it is not embedded in the iframe URL or automatically included in the bridge session.

Provider credentials belong on your backend
Do not place ShipStation keys or other provider secrets in installation config when an iframe can read that config. Collect credentials through an authenticated setup flow, encrypt them in publisher-controlled storage, and retain only a non-secret connection status or provider account label in display configuration.
Update config
# Update the installation configuration after install.
curl -X PATCH https://api.usethrottle.dev/api/v1/installations/inst_xxx/config \
  -H "Authorization: Bearer <dashboard-session>" \
  -H "X-Throttle-Application-Id: <applicationId>" \
  -H "content-type: application/json" \
  -d '{ "config": { "syncInterval": 120, "notifyOnNew": false } }'
# → { data: { id: "inst_xxx", config: { ... }, ... } }
Read config from an iframe
import { createBridge } from '@usethrottle/extension-bridge';
const bridge = createBridge({ targetOrigin: 'https://app.usethrottle.dev' });
const ctx = await bridge.ready;
// The bridge context gives you the installation id, not the config object.
// If your iframe needs runtime config, request extensions:read in the manifest
// and read the installation record through the scoped API client.
const installs = await bridge.api.get('/api/v1/installations') as {
  data: Array<{ id: string; config: Record<string, unknown> }>;
};
const install = installs.data.find((item) => item.id === ctx.installationId);
const config = install?.config ?? {};
Config is separate from the bridge context
bridge.ready resolves with user, workspace, application, environment, installation id, version, role, and scopes. It intentionally does not include config or configSchema. If a browser iframe needs config, give the extension an explicit read scope and fetch the installation record. Keep secrets that should never reach browser JavaScript on your extension backend instead.
Config is not the signing secret
Do not store the webhookSigningSecret in config — the config object may be readable by anyone with extensions:read scope. Store the signing secret in your own secrets manager.

Configuration schema

Extension authors can publish a configSchema to describe the settings an installer should provide. The dashboard authoring flow includes a field builder for simple string, number, and boolean settings plus an advanced JSON editor for custom JSON Schema. During install and on the management page, Throttle uses this schema to render the configuration form. Empty schemas are treated as “no configurable options.”

configSchema
{
  "type": "object",
  "properties": {
    "apiKey": {
      "type": "string",
      "title": "Provider API key",
      "description": "Issued by the upstream provider"
    },
    "syncInterval": {
      "type": "number",
      "title": "Sync interval"
    },
    "notifyOnNew": {
      "type": "boolean",
      "title": "Notify on new records"
    }
  },
  "required": ["apiKey"]
}
Schema support
The builder emits a JSON Schema object with type: "object", properties, and optional required. Use the advanced JSON mode for nested objects, arrays, enums, or provider-specific validation.
Schema lifecycle
configSchema lives on the workspace catalog extension while you are editing it. Creating a version snapshots that schema into the immutable version record. Installs use the pinned version's schema to collect per-install config values. Publishing a new schema does not rewrite existing installs until they upgrade to a version that carries it.

Environment-scoped installs

Installs are per environment. A UAT install receives only UAT events and gets an API key for that environment; a production install receives only production events and gets a production-environment API key. The installs are completely independent — each has its own config, its own API key, and its own event delivery endpoint.

List and inspect installs

List installations
# List all installations for the current application + environment.
GET /api/v1/installations
  -H "Authorization: Bearer <dashboard-session>"
  -H "X-Throttle-Application-Id: <applicationId>"
  -H "X-Throttle-Environment-Id: <workspace-environment-uuid>"
# Response shape: paginated list
# {
#   "data": [ { "id": "inst_...", "extensionId": "ext_...", "status": "active", ... } ],
#   "meta": { "requestId": "...", "pagination": { "cursor": null, "hasMore": false } }
# }

Install status values: active, suspended. Suspended installs stop receiving events and their launch-token endpoint returns 409 installation_not_active. Delivery logs are accessible at GET /api/v1/installations/:id/deliveries.

Uninstall

Uninstalling removes the install row for the current application and environment. The extension catalog entry and other applications' installs are not affected. The API key and signing secret issued at install time are immediately revoked.

Uninstall
# Uninstall removes the active install for this application + environment.
# The extension catalog entry and other applications' installs are unaffected.
curl -X POST https://api.usethrottle.dev/api/v1/extensions/ext_xxx/uninstall \
  -H "Authorization: Bearer <dashboard-session>" \
  -H "X-Throttle-Application-Id: <applicationId>"
# → 204 No Content