App SDK Reference (@brainerce/app-sdk)

Manifest builder, connector / payment / shipping contracts, scopes, events and webhook verification for marketplace apps built on @brainerce/app-sdk.

@brainerce/app-sdk is not published, and the app platform is not open yet. The install command below fails with a 404 from the npm registry, and Brainerce does not currently accept third-party apps: the option is hidden in the product. This page documents the intended shape of the app SDK rather than a package you can install today. If you are building a storefront, the package you want is brainerce, which is published and supported. See the note under the install command.

Complete API reference for @brainerce/app-sdk, the SDK for building marketplace apps (connectors, payment providers, shipping carriers) that plug into Brainerce.

npm install @brainerce/app-sdk

This is not the storefront SDK. If you are building a store with a catalog, cart, checkout, payments and orders, you want the separate brainerce package (npm install brainerce, class BrainerceClient). Its guided reference is the Core Integration Guide and its full method reference ships as the package README on npm. Nothing on this page (ManifestBuilder, BrainerceAppClient, the connector contracts) exists in that package.


ManifestBuilder

Fluent builder for constructing app manifests. Validates on build().

import { ManifestBuilder } from '@brainerce/app-sdk';

const manifest = new ManifestBuilder()
  .name('My App')
  .version('1.0.0')
  .connectorIntegration('https://my-app.com', {
    pushProduct: '/sync/push-product',
    pullOrders: '/sync/pull-orders',
    connectionStatus: '/sync/connection-status',
    reconcile: '/sync/reconcile',
  })
  .hooks({
    onInstall: '/hooks/on-install',
    onUninstall: '/hooks/on-uninstall',
    onConfigUpdate: '/hooks/on-config-update',
  })
  .health('/health')
  .eventSubscriptions(['product.created', 'order.created'])
  .build(); // throws ManifestValidationError if invalid

Methods

MethodParametersDescription
name(name)stringSet the app name (required)
version(version)stringSet version in semver format, e.g. "1.0.0" (required)
connectorIntegration(serviceUrl, endpoints)string, ConnectorEndpointsConfigure as connector
paymentIntegration(serviceUrl, endpoints)string, PaymentEndpointsConfigure as payment provider
shippingIntegration(serviceUrl, endpoints)string, ShippingEndpointsConfigure as shipping provider
customIntegration(serviceUrl, contract?)string, Record<string, unknown>?Configure as custom integration
paymentOAuth(oauth)OAuthConfigAdd OAuth to the payment contract. Call after paymentIntegration(), since it is a no-op if no payment contract exists yet.
clientSdk(config)PaymentClientSdkDeclare how the storefront renders your payment UI (renderType + script/global/container). Without it the checkout has nothing to mount. displayModes lists every mode you can serve; a storefront's preferredRenderType is honoured only from that list, and the core overwrites any per-intent renderType you return outside it.
supportedCountries(countries)string[]ISO 3166-1 alpha-2 codes where the app is available (empty = everywhere)
excludedCountries(countries)string[]Codes where the app is NOT available. Takes precedence over supportedCountries
connectionFlow(flow)ConnectionFlowWhich connect UI the dashboard renders for this payment app
hooks(hooks)ManifestHooksSet lifecycle hooks
health(path)stringSet health check endpoint path
eventSubscriptions(events)string[]Subscribe to domain events
build()noneValidate and return AppManifest. Throws ManifestValidationError on failure.
toJSON()noneReturn current state without validation

validateManifest(manifest)

Standalone validation function:

import { validateManifest } from '@brainerce/app-sdk';

const result = validateManifest(myManifest);
if (!result.valid) {
  console.error(result.errors);
  // [{ field: 'name', message: 'name is required and must be a non-empty string' }]
}

BrainerceAppClient

HTTP client for your app to communicate with the Brainerce API. Uses the installation token received during app installation.

import { BrainerceAppClient } from '@brainerce/app-sdk';

const client = new BrainerceAppClient({
  token: 'app_inst_abc123', // Required: installation token
  installationId: 'inst_01HXYZ', // Required: must match the token's installation
  baseUrl: 'https://api.brainerce.com', // Optional (this is the default)
  timeout: 30000, // Optional: request timeout in ms (default: 30000)
});

installationId is required because every route the client calls is scoped under /api/v1/installations/:installationId/..., and the backend rejects a request whose route installationId doesn't match the token's own installation.

There is currently no app-facing endpoint to read or write installation config, or to report health status — BrainerceAppClient only covers secrets and resource mappings (below). Installation config is merchant-owned and set from the dashboard's settings form; your app receives it as config on every BaseAppRequest instead of fetching it itself.

Installation Secrets

// Write secrets (write-only — you cannot read secrets back). Existing keys
// not included in the call are preserved.
await client.updateSecrets({ apiKey: 'new-key-123' });

Resource Mappings

Track which Brainerce resources map to which external platform resources:

// Create or update a mapping (idempotent; recommended for sync loops)
await client.upsertResourceMapping({
  resourceType: 'product',
  internalId: 'brainerce-product-id',
  externalId: '12345',
  externalUrl: 'https://platform.com/products/12345',
  platformCode: 'tiktok', // Required: matches an installed app's `App.platformCode`
  syncStatus: 'synced', // 'synced' | 'pending' | 'error' | 'stale'
});

// List mappings for a resource type (paginated)
const { data, meta } = await client.getResourceMappings({ resourceType: 'product' });
// data: Array<{ id, storeId, installationId, resourceType, internalId, externalId,
//               externalUrl, platformCode, syncStatus, lastSyncedAt, lastSyncError,
//               metadata, createdAt, updatedAt }>
// meta: { page, limit, total, totalPages }

syncStatus values:

ValueMeaning
syncedThe mapping is up to date with the external platform.
pendingThe mapping was just created/queued and hasn't synced yet.
errorThe last sync attempt failed — see lastSyncError.
staleThe mapping synced before, but the underlying resource has since changed and is due for a re-sync.

Generic Request

For any custom API call:

const result = await client.request<MyType>(
  'GET',
  `/api/v1/installations/${installationId}/mappings`
);

Contract Types

All request/response interfaces for app-to-core communication. Every request extends BaseAppRequest.

BaseAppRequest

interface BaseAppRequest {
  storeId: string;
  installationId: string;
  config: Record<string, unknown>;
  secrets: Record<string, unknown>;
}

Connector Contracts

PushProductRequest / PushProductResponse

interface PushProductRequest extends BaseAppRequest {
  productId: string;
  productData: Record<string, unknown>; // Brainerce product format
  externalId?: string; // If updating existing
}

interface PushProductResponse {
  externalId: string;
  externalUrl?: string;
}

PullOrdersRequest / PullOrdersResponse

interface PullOrdersRequest extends BaseAppRequest {
  since?: string; // ISO 8601 date — only fetch orders after this
  page?: number;
  limit?: number;
}

interface PullOrdersResponse {
  orders: Record<string, unknown>[];
  hasMore?: boolean;
  nextPage?: number;
}

ConnectionStatusRequest / ConnectionStatusResponse

type ConnectionStatusRequest = BaseAppRequest;

interface ConnectionStatusResponse {
  connected: boolean;
  scopes?: string[];
  hasRequiredScopes?: boolean;
  details?: Record<string, unknown>;
  error?: string;
}

ReconcileRequest / ReconcileResponse

interface ReconcileRequest extends BaseAppRequest {
  resourceType: string; // "product", "order", etc.
  since?: string;
}

interface ReconcileResponse {
  reconciled: number;
  created: number;
  updated: number;
  deleted: number;
  errors?: string[];
}

Payment Contracts

CreatePaymentIntentRequest / PaymentIntentResponse

interface CreatePaymentIntentRequest extends BaseAppRequest {
  amount: number;
  currency: string;
  returnUrl?: string;
  metadata?: Record<string, unknown>;
  customerId?: string;
  description?: string;
}

interface PaymentIntentResponse {
  id: string;
  /**
   * The provider's payment IDENTIFIER. Put a URL here only if it genuinely is
   * one; storefronts read `clientSdk.renderArg` first for anything they have
   * to open or render.
   */
  clientSecret: string;
  status:
    | 'requires_payment_method'
    | 'requires_confirmation'
    | 'requires_action'
    | 'processing'
    | 'succeeded'
    | 'canceled';
  amount: number;
  currency: string;
  metadata?: Record<string, unknown>;
  /** Runtime clientSdk overrides, merged with your manifest config by core. */
  clientSdk?: {
    initConfig?: Record<string, unknown>;
    /**
     * What the storefront hands to the render step: the iframe `src`, the
     * redirect target, or the argument passed to `renderMethod`. Overrides
     * `clientSecret`. If your provider returns a hosted payment page, set it
     * here.
     */
    renderArg?: string;
  };
}

renderArg is what every storefront reads first when it needs a URL, with clientSecret as the fallback. A hosted page or iframe URL left only in clientSecret appears to work and breaks as soon as clientSecret carries a real identifier instead. Full reasoning in renderArg vs clientSecret.

ConfirmPaymentRequest

interface ConfirmPaymentRequest extends BaseAppRequest {
  intentId: string;
  paymentMethodId?: string;
}

RefundRequest / RefundResponse

interface RefundRequest extends BaseAppRequest {
  intentId: string;
  amount?: number; // Partial refund amount (omit for full refund)
  reason?: string;
  metadata?: Record<string, unknown>;
}

interface RefundResponse {
  id: string;
  paymentId: string;
  amount: number;
  currency: string;
  status: 'pending' | 'succeeded' | 'failed' | 'canceled';
}

OAuth Contracts

OAuthStartRequest / OAuthStartResponse

interface OAuthStartRequest extends BaseAppRequest {
  callbackUrl: string; // URL to redirect back to after authorization
}

interface OAuthStartResponse {
  redirectUrl: string; // Full URL to redirect the user to
}

OAuthCallbackRequest / OAuthCallbackResponse

interface OAuthCallbackRequest extends BaseAppRequest {
  originalPayload: Record<string, unknown>; // Query params/body from the OAuth redirect
  originalHeaders: Record<string, string>;
}

interface OAuthCallbackResponse {
  success: boolean;
  secrets?: Record<string, unknown>; // Credentials to store (encrypted automatically)
  config?: Record<string, unknown>; // Config updates
  error?: string;
}

Hook Contracts

type HookRequest = BaseAppRequest;

interface HookResponse {
  success: boolean;
  message?: string;
}

Inbound Webhook Contract

For processing webhooks from the external platform, forwarded through Brainerce:

interface InboundWebhookRequest extends BaseAppRequest {
  originalPayload: unknown;
  originalHeaders: Record<string, string>;
}

interface InboundWebhookResponse {
  received: boolean;
  processed?: boolean;
  data?: Record<string, unknown>;
}

Manifest Types

type IntegrationType = 'payment' | 'connector' | 'shipping' | 'custom';

interface AppManifest {
  name: string;
  version: string; // Semver: "1.0.0"
  integration: IntegrationConfig;
  hooks?: ManifestHooks;
  health?: string; // e.g. "/health"
  eventSubscriptions?: string[]; // e.g. ["product.created", "order.updated"]
}

interface IntegrationConfig {
  type: IntegrationType;
  serviceUrl: string; // Where your service runs
  paymentContract?: { endpoints: PaymentEndpoints };
  connectorContract?: { endpoints: ConnectorEndpoints; oauth?: OAuthConfig };
  shippingContract?: { endpoints: ShippingEndpoints };
  customContract?: Record<string, unknown>;
}

Endpoint Types

interface ConnectorEndpoints {
  pushProduct?: string;
  pullOrders?: string;
  connectionStatus?: string;
  reconcile?: string;
}

interface PaymentEndpoints {
  createPaymentIntent: string; // Required
  confirmPayment: string; // Required
  refund: string; // Required
  connectionStatus?: string;
}

interface ShippingEndpoints {
  getRates?: string;
  createShipment?: string;
}

interface OAuthConfig {
  startPath: string; // e.g. "/oauth/start"
  callbackPath: string; // e.g. "/oauth/callback"
}

interface ManifestHooks {
  onInstall?: string;
  onUninstall?: string;
  onConfigUpdate?: string;
}

Scopes

type AppScope =
  | 'products:read'
  | 'products:write'
  | 'orders:read'
  | 'orders:write'
  | 'inventory:read'
  | 'inventory:write'
  | 'customers:read'
  | 'customers:write';

// Full list
import { APP_SCOPES } from '@brainerce/app-sdk';

// Human-readable descriptions
import { SCOPE_DESCRIPTIONS } from '@brainerce/app-sdk';
// { 'products:read': 'Read product data', 'products:write': 'Create and update products', ... }

Domain Events

type DomainEventType =
  | 'product.created'
  | 'product.updated'
  | 'product.deleted'
  | 'order.created'
  | 'order.updated'
  | 'inventory.changed';

interface DomainEvent {
  event: DomainEventType;
  storeId: string;
  installationId: string;
  timestamp: string; // ISO 8601
  data: Record<string, unknown>; // Event-specific payload
}

// Full list
import { DOMAIN_EVENT_TYPES } from '@brainerce/app-sdk';

Webhook Verification

If your app uses createIntegrationApp() from @brainerce/integration-shared, signature verification is automatic. Just set the environment variable:

# Ed25519 public key from the Brainerce platform (NOT sensitive)
BRAINERCE_PUBLIC_KEY=MCowBQYDK2VwAyEA...

All incoming requests are verified before reaching your handlers. No code needed.

Alternative: Manual HMAC Verification (via app-sdk)

For apps that don't use integration-shared (e.g., standalone services receiving event webhooks), you can verify manually with HMAC:

import { verifyWebhookSignature } from '@brainerce/app-sdk';

app.post('/webhooks', (req, res) => {
  const isValid = verifyWebhookSignature({
    body: req.rawBody, // Raw request body as string
    signature: req.headers['x-brainerce-signature'], // Hex-encoded HMAC
    timestamp: req.headers['x-timestamp'], // ISO 8601
    secret: savedWebhookSecret, // From your DB (received during onInstall)
    maxAgeMs: 300_000, // Optional: max age (default 5 min)
  });

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Process the webhook...
});

Important: Store the webhookSecret received during onInstall in your database, not in memory. It must survive restarts.

Signature format: HMAC-SHA256(secret, "${timestamp}.${body}") → hex-encoded. Uses timing-safe comparison and rejects expired or future timestamps.


Errors

import { AppClientError, ManifestValidationError } from '@brainerce/app-sdk';

// Thrown by BrainerceAppClient on HTTP errors
class AppClientError extends Error {
  status: number;
  data?: unknown;
}

// Thrown by ManifestBuilder.build() on validation failure
class ManifestValidationError extends Error {
  errors: Array<{ field: string; message: string }>;
}