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). The keys you send
// are merged over the stored ones: keys not included in the call are preserved,
// so sending only a refreshed accessToken keeps the stored refreshToken.
await client.updateSecrets({ apiKey: 'new-key-123' });
// A key sent as null is deleted.
await client.updateSecrets({ oldApiKey: null });The merged secrets are capped at 64 KB. If two writes to the same installation race, one answers 409; retry it.
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;
metadata?: Record<string, unknown>;
}intentId is the id your createPaymentIntent returned, bound to the checkout
by the platform. paymentMethodId is the storefront's word: the platform
JSON-stringifies whatever the storefront posted to its public confirm route and
forwards it verbatim, so treat it as untrusted input. metadata is the
platform's word: the metadata object your own createPaymentIntent response
carried, persisted and echoed back server-to-server. A credential you minted at
intent time and need to verify the payment with your provider (for example a process token you later pass to the provider's payment-info lookup) belongs in metadata, never in the
storefront payload. Answer succeeded only after your provider confirms the
payment; every other outcome must be a non-succeeding status such as
processing.
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>;
}Additional contract types
These types are exported by @brainerce/app-sdk and complete the contracts above.
// Itemized invoice lines the core sets on CreatePaymentIntentRequest.lineItems.
// Forward them to your invoicing endpoint as they are: the core guarantees that
// price * quantity across all lines equals `amount` (major units).
interface PaymentLineItem {
name: string;
quantity: number;
price: number; // unit price, same currency as the payment, major units
sku?: string;
}
// CreatePaymentIntentRequest also carries (all optional):
// lineItems?: PaymentLineItem[];
// metadata?: Record<string, unknown>; // core sets checkoutId, storeId, fullName, phone, email, successUrl, cancelUrl, notifyUrl
// returnUrl?: string; customerId?: string; description?: string;
// RefundRequest also carries providerData, the original payment's Payment.providerData
// verbatim. Read provider lookup keys (for example a document number needed to cancel an
// invoice) from it, not from metadata.
// providerData?: Record<string, unknown>;
// RefundResponse also carries the credit note a provider issued with the refund. Report
// only what the provider returned: the number is a legal document reference.
// creditNoteNumber?: string;
// creditNoteUrl?: string;
// Connector sync contract: inventory, coupons and deletes.
interface PushInventoryRequest extends BaseAppRequest {
productId: string;
variantId?: string;
stock: number;
trackingMode: string;
externalId?: string;
}
interface PushInventoryResponse {
success: boolean;
externalId?: string;
}
interface PushCouponRequest extends BaseAppRequest {
couponId: string;
couponData: Record<string, unknown>;
externalId?: string;
}
interface PushCouponResponse {
externalId: string;
externalUrl?: string;
}
interface DeleteResourceRequest extends BaseAppRequest {
resourceType: string;
externalId: string;
internalId?: string;
}
interface DeleteResourceResponse {
success: boolean;
error?: string;
}Payment webhook events
A payment app can answer an inbound webhook with a typed event. When event is present the platform queues it for order creation and status updates.
type PaymentWebhookEventType =
| 'payment.succeeded'
| 'payment.failed'
| 'payment.refunded'
| 'payment.partially_refunded'
| 'payment.disputed'
| 'payment.unknown';
interface PaymentMethodDetails {
brand?: string;
last4?: string;
expMonth?: number;
expYear?: number;
[key: string]: unknown;
}
interface NormalizedPaymentEvent {
type: PaymentWebhookEventType;
// At least one lookup key, so the core can find the checkout or payment.
checkoutId?: string;
paymentIntentId?: string;
paymentExternalId?: string;
// payment.succeeded: used for order creation.
payment?: {
externalId: string;
amount: number; // human-readable units, 50.00 and not 5000 cents
currency: string; // ISO 4217
paymentMethod: string; // normalized: "credit_card", "apple_pay", "google_pay", "paypal", ...
paymentMethodDetails?: PaymentMethodDetails;
};
// payment.refunded and payment.partially_refunded.
refund?: { amount: number; externalId?: string; reason?: string };
// payment.failed.
error?: { code?: string; message?: string };
// Stored on Payment.providerData and never parsed by the core.
providerData?: Record<string, unknown>;
}
interface NormalizedPaymentWebhookResponse extends InboundWebhookResponse {
received: boolean;
processed: boolean;
event?: NormalizedPaymentEvent; // set when the webhook parsed into a payment event
}
// Invoice data a webhook can return in data.invoice. Any webhook may return it, not only
// an invoice-notify one; it is stored on Payment.providerData.
interface WebhookInvoicePayload {
processId: string; // matches Payment.checkoutId or a provider process id
invoiceNumber?: string;
invoiceUrl: string;
}InboundWebhookResponse.data may also carry configUpdate (merged into the installation's config) and secretsUpdate (encrypted and merged into its secrets). A key is stored only when it is declared in the app's config schema or secrets schema (or is one of the platform's own metadata keys) and is not marked read-only; one refused key drops the whole configUpdate (and, separately, the whole secretsUpdate), silently, while the webhook still answers processed: true.
Manifest Types
type IntegrationType = 'payment' | 'connector' | 'shipping' | 'custom';
interface AppManifest {
name: string;
version: string; // Semver: "1.0.0"
integration: IntegrationConfig;
clientSdk?: PaymentClientSdk; // how the storefront renders your payment UI, see below
hooks?: ManifestHooks;
health?: string; // e.g. "/health"
eventSubscriptions?: string[]; // e.g. ["product.created", "order.updated"]
supportedCountries?: string[]; // ISO 3166-1 alpha-2; empty = all countries
excludedCountries?: string[]; // takes precedence over supportedCountries
connectionFlow?: ConnectionFlow; // 'oauth' | 'api-key' | 'registration': which connect UI to render
capabilities?: AppCapabilities; // explicit flags, overrides the endpoint-presence inference
// Allowlist of extra endpoint paths callable through the generic
// POST /api/apps/:installationId/call/:path proxy. Paths are relative to serviceUrl and
// have no leading slash ("registration/submit"). Any path not listed is rejected.
publicEndpoints?: string[];
}
// Declarative capability flags. A capability defaults to true when its endpoint exists
// (refund endpoint present means refund: true); an explicit false force-disables it.
interface AppCapabilities {
refund?: boolean;
openTerminal?: boolean;
installments?: boolean; // the app can offer credit-card installments at checkout
}
type ConnectionFlow = 'oauth' | 'api-key' | 'registration';
interface PaymentClientSdk {
renderType: 'sdk-widget' | 'iframe' | 'redirect' | 'sandbox' | 'embedded-fields'; // the default mode
displayModes?: Array<'sdk-widget' | 'iframe' | 'redirect' | 'sandbox' | 'embedded-fields'>;
scriptUrl?: string; // main SDK script to load
globalName?: string; // global set by that script, the name your provider's script defines
initMethod?: string; // default 'init'
renderMethod?: string; // called to render the widget
containerId?: string; // DOM element ID of the widget container
initConfig?: Record<string, unknown>; // passed to the init method
renderArg?: string; // argument for the render method, overrides clientSecret when set
additionalScripts?: Array<{ url: string; optional?: boolean }>; // for example the Apple Pay SDK
bodyStyles?: string; // CSS injected into <head> while the SDK is active
embeddedFieldsUrl?: string; // embedded-fields: base URL the sensitive-field iframes load from
embeddedFieldsSessionId?: string; // embedded-fields: provider session id for the iframe handshake
embeddedFieldsProvider?: 'cardcom'; // embedded-fields: reserved. 'cardcom' is the only value the contract declares, and no payment provider sends it today
}
interface IntegrationConfig {
type: IntegrationType;
serviceUrl: string; // Where your service runs
paymentContract?: { endpoints: PaymentEndpoints; oauth?: OAuthConfig };
connectorContract?: { endpoints: ConnectorEndpoints; oauth?: OAuthConfig };
shippingContract?: {
endpoints: ShippingEndpoints;
serviceType?: 'parcel' | 'on_demand'; // default 'parcel'; 'on_demand' = a live courier (no label, a time-limited quote)
capabilities?: ShippingCapabilities; // what makes this carrier different: see "Shipping contract" below
oauth?: OAuthConfig; // omit for an API-key carrier with no connect flow
};
receiptContract?: { endpoints: ReceiptEndpoints }; // optional, manifest JSON only for now: see "Receipt contract" below
customContract?: Record<string, unknown>;
}Endpoint Types
interface ConnectorEndpoints {
pushProduct?: string;
pushInventory?: string;
pullProducts?: string;
pullOrders?: string;
connectionStatus?: string;
reconcile?: string;
deleteResource?: string;
pushCategory?: string;
pullCategories?: string;
pushTag?: string;
pullTags?: string;
pushBrand?: string;
pullBrands?: string;
pushAttribute?: string;
pullAttributes?: string;
pushOrderNotes?: string;
pushOrderStatus?: string;
pushShipping?: string;
getRefunds?: string;
createRefund?: string;
updateOrderFields?: string;
pushCoupon?: string;
}
interface PaymentEndpoints {
createPaymentIntent: string; // Required
confirmPayment: string; // Required
refund: string; // Required
connectionStatus?: string;
}
interface ShippingEndpoints {
getRates?: string; // rate shopping at checkout and in the dashboard label flow
createShipment?: string; // buys a label for a previously quoted rate
trackShipment?: string; // polled for tracking (needed when trackingMode is 'poll' or 'both')
voidShipment?: string; // optional: absent means "this app cannot cancel"
createReturnShipment?: string; // optional, same terms
validateCredentials?: string; // optional: check a credential before it is saved, no side effects
getLabel?: string; // for labelDelivery: 'inline' apps
listPickupPoints?: string; // optional: partner shops and lockers near the shipping address (see below)
}
interface ReceiptEndpoints {
createDonationReceipt: string; // e.g. "/receipts/donation"
}
interface OAuthConfig {
startPath: string; // e.g. "/oauth/start"
callbackPath: string; // e.g. "/oauth/callback"
}
interface ManifestHooks {
onInstall?: string;
onUninstall?: string;
onConfigUpdate?: string;
}Receipt contract (donation receipts)
Optional, for an app that keeps an organisation's receipt books (donation receipts and their refund counterparts). An installed app whose manifest declares integration.receiptContract.endpoints.createDonationReceipt is offered to the merchant as a receipt provider in the dashboard (Donations page, Receipts tab). Declare it in the manifest JSON: the IntegrationConfig type exported by @brainerce/app-sdk does not list receiptContract yet, but the platform reads it from the stored manifest. The platform's workers then call that endpoint once per receipt, with CreateDonationReceiptRequest and expecting CreateDonationReceiptResponse (both in @brainerce/integration-shared).
Nothing else in the platform names a provider, so declaring the endpoint is all it takes to join. A published app that declares it is listed in the dashboard as one the merchant can connect, and once installed it is offered as a receipt provider. A payment gateway can declare it too. The app is chosen like this:
- The merchant's choice wins. If the merchant picked an app on the Receipts tab, every receipt is issued by it.
- Otherwise the app that charged the gift, when it declares the endpoint, so a gateway that also keeps the books is used without setup (and a gift it already wrote a document for is held for review instead of getting a second one). Failing that, the store's first installed app that declares it.
- A gift your app charged and already wrote a document for is held, not doubled. Report that document from your payment webhook as
providerData.providerDocument = { number, type? }. The receipt then waits for the merchant to record the existing document or issue the donation receipt anyway. (An olderproviderData.documentInfoshape is read the same way.) - A refund credit always goes to the app that issued the receipt it reverses. A credit citing another provider's document number is not a valid credit, so if that app is no longer installed the credit fails visibly instead of going elsewhere. Your endpoint receives the original document's number and type in
originalDocument.
interface CreateDonationReceiptRequest {
kind: 'RECEIPT' | 'REFUND';
externalId: string; // the platform's receipt id: store it as the document's external id
donorName: string;
donorTaxId: string; // the donor's national ID or company number, required on a receipt
donorEmail?: string;
donorPhone?: string;
sendByEmail: boolean; // the provider emails the document to the donor itself
amount: number; // MAJOR units (180.00), not cents
currency: string;
description: string; // one line, e.g. "Donation: Building fund"
language: 'he' | 'en';
paidAt: string; // ISO 8601: when the gift was paid (or refunded)
payment: { provider: string; reference?: string; sameProviderDealNumber?: number };
originalDocument?: { number: number; type: string }; // kind REFUND: the receipt it reverses
}
interface CreateDonationReceiptResponse {
documentNumber: number;
documentType: string;
documentUrl?: string | null;
}The status code is the contract. A receipt is a legal record, so the platform never guesses:
| Your answer | Meaning | What the platform does |
|---|---|---|
| 2xx | A document exists. Return its number. | Marks the receipt issued. |
| 4xx | The provider definitely created nothing. | Marks it "Not issued" with your message; the merchant may try again after fixing it. |
| 5xx | You cannot tell (timeout, ambiguous provider answer). | Marks it "Check your books". It is never re-issued automatically. |
Answer 4xx only when you are certain nothing was written; when in doubt, answer 5xx. A 4xx that hid a created document would put two receipts in the books for one gift. The platform also calls nothing while the app installation's config is in test mode, so a real donor never reaches a test account.
Shipping contract (carrier apps)
A carrier is an app with type: 'shipping'. The core never branches on a carrier's name: whatever makes a carrier different is declared in its manifest, and the core adapts. Every request below extends BaseAppRequest; types live in @brainerce/integration-shared (GetShippingRatesRequest, CreateShipmentRequest, ListPickupPointsRequest, ...).
| Endpoint | Request → response |
|---|---|
getRates | { origin, destination, parcels[], checkoutCurrency?, customs? } → { rates: NormalizedShippingRate[] } |
createShipment | { shipmentId, rateObjectId, orderId, toAddress, fromAddress, parcels[], labelFormat?, customs?, cashOnDelivery?, pickupPoint? } → { externalId, trackingNumber, carrier, labelUrl?, labelContent?, trackingUrl?, ... } |
trackShipment | a shipment reference → its current tracking state |
voidShipment, createReturnShipment | Optional. Declaring neither means the dashboard offers neither |
validateCredentials | Optional. Check a credential before it is saved, with no side effects |
getLabel | Only for labelDelivery: 'inline': { shipmentId, externalId, trackingNumber } → { labelContent: { base64, format } } |
listPickupPoints | Optional. { country, city?, postalCode?, latitude?, longitude?, limit } → { points: PickupPoint[] } |
Capabilities. Declare behaviour differences in shippingContract.capabilities. Every field is optional, and the default is what every app did before the field existed:
| Field | Values (default first) | Meaning |
|---|---|---|
trackingMode | 'push', 'poll', 'both' | How the core learns a shipment moved: the carrier calls the platform, the core polls trackShipment, or both (webhook primary, polling as a safety net) |
trackingCadence | 'standard', 'relaxed' | Polling rhythm. relaxed suits a courier whose status moves a few times a day (about 70% fewer calls) |
quoting | 'live', 'none' | 'none' for a carrier with no rate API: never quoted at checkout, labels are bought without a rate, and the shopper pays the store's own shipping zones |
labelDelivery | 'url', 'inline' | 'inline' when fetching the label needs the merchant's credential: the app returns the bytes and the core stores them |
billing | 'prepaid_wallet', 'contract' | prepaid_wallet drains a balance the dashboard warns about; contract is invoiced by the carrier |
requiredRecipientFields | Array<'phone' | 'email' | 'name'> | Recipient fields the carrier refuses to ship without, checked before anything is bought |
originAddress | 'sent', 'carrier_side' | 'carrier_side' when the carrier collects from an address held in the merchant's contract and reads none from the platform |
ManifestBuilder.shippingIntegration() sets the endpoints only. Put serviceType, capabilities and oauth on manifest.integration.shippingContract yourself after build(). The @brainerce/app-sdk ShippingCapabilities type does not yet list originAddress or trackingCadence, and ShippingEndpoints does not yet list listPickupPoints: the platform reads all three from the manifest JSON.
Pickup points. An app that lists listPickupPoints lets a storefront offer carrier pickup (the shopper must be on an SDK client that declares features: ['pickup-points'], see Choose a carrier pickup point). The platform only ever sends createShipment a pickupPoint that this app listed, so an app that declares the endpoint must deliver to it. Return no secrets in a point.
A refusal is a 4xx; an unknown outcome is not. The platform decides what a failed createShipment means from your HTTP status: a 4xx other than 408 or 429 means the carrier refused and nothing exists (the order stays retryable); a 5xx, 408, 429 or a timeout means the shipment may exist, so it is not retried blindly. Answer an unconfigured request with a 4xx, never echo a credential in any response, answer { rates: [] } rather than invent a price when you have no rate API, and derive the carrier's references from shipmentId so a retry after a lost reply finds the shipment instead of buying a second one.
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 }>;
}Storefront SDK Reference (brainerce)
Every reachable method on BrainerceClient, the storefront SDK. Grouped by domain, with the auth mode each one needs, ordered by the mandatory storefront flow.
Storefront Bot (AI chat widget)
Add Brainerce's AI shopping assistant to any storefront with one line of code. The bot answers product questions, shows recommendation cards, handles cart adds, and escalates to the merchant team when needed.