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-sdkis 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 isbrainerce, 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-sdkThis is not the storefront SDK. If you are building a store with a catalog, cart, checkout, payments and orders, you want the separate
brainercepackage (npm install brainerce, classBrainerceClient). 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 invalidMethods
| Method | Parameters | Description |
|---|---|---|
name(name) | string | Set the app name (required) |
version(version) | string | Set version in semver format, e.g. "1.0.0" (required) |
connectorIntegration(serviceUrl, endpoints) | string, ConnectorEndpoints | Configure as connector |
paymentIntegration(serviceUrl, endpoints) | string, PaymentEndpoints | Configure as payment provider |
shippingIntegration(serviceUrl, endpoints) | string, ShippingEndpoints | Configure as shipping provider |
customIntegration(serviceUrl, contract?) | string, Record<string, unknown>? | Configure as custom integration |
paymentOAuth(oauth) | OAuthConfig | Add OAuth to the payment contract. Call after paymentIntegration(), since it is a no-op if no payment contract exists yet. |
clientSdk(config) | PaymentClientSdk | Declare 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) | ConnectionFlow | Which connect UI the dashboard renders for this payment app |
hooks(hooks) | ManifestHooks | Set lifecycle hooks |
health(path) | string | Set health check endpoint path |
eventSubscriptions(events) | string[] | Subscribe to domain events |
build() | none | Validate and return AppManifest. Throws ManifestValidationError on failure. |
toJSON() | none | Return 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:
| Value | Meaning |
|---|---|
synced | The mapping is up to date with the external platform. |
pending | The mapping was just created/queued and hasn't synced yet. |
error | The last sync attempt failed — see lastSyncError. |
stale | The 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
Recommended: Ed25519 (via integration-shared)
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 }>;
}