Developers
Default PATCH path used by the SDK:
/api/v1/contacts/update
You can override the path in the second argument to submitContactUpdate (e.g. a custom path option) if your deployment uses a different route, but the response should still match the contract: status in processed_globally | pending_user_review | rejected, with optional message and correlationId for support and audit.
Direct Connect for partner merchants is experimental and coming soon. APIs and UI below describe the planned integration surface so you can prepare; availability in shared environments will follow.
Mint opaque browser tokens with POST /api/v1/direct-connect-tokens (Bearer API key). Primary stores (tenant_id, external_user_id) in tenant_link_token and returns a linkToken for connect/disconnect URLs — no internal tenant id in the token or SDK types. Demo partner route that consumes this pattern: GET /api/anemone/connection.
Requests use Authorization: Bearer <api-key> and Content-Type: application/json. An optional idempotency-key header is supported for safe retries.
externalUserId — the user in your app (never an Anemone-only id)kind — e.g. addressaddress — optional line1, line2, city, region, postalCode, countryCode, labelFor full types, import from anemone-server-js in your TypeScript project; source lives in the anemone-server-js package. Partner-side push example: pushContactUpdatesToPrimary.
When live delivery fails (timeout, 5xx, or rate limit), the SDK can POST the same body to:
/api/v1/contacts/update/queue
Response: { status: "queued", queueId?, correlationId? }. Primary drains this queue twice daily, on restart, and when triggered from the admin dashboard (invite required).
Your app may use different property names than Anemone (for example street instead of line1, or fullName instead of separate firstName / lastName). The SDK ships helpers in anemone-server-js so you can map in both directions without hand-rolling transforms in every route.
Define a rename object keyed by Anemone field paths. Values are your merchant property names. Nested address fields use dot notation (address.line1, address.region, …).
import {
buildContactUpdateFromMerchant,
mapContactEventToMerchant,
type ContactInfoMapping,
} from "anemone-server-js";
type MerchantContact = {
street: string;
street2?: string;
city: string;
state: string;
zip: string;
countryCode: string;
phone: string;
};
const mapping: ContactInfoMapping<MerchantContact> = {
rename: {
"address.line1": "street",
"address.line2": "street2",
"address.city": "city",
"address.region": "state",
"address.postalCode": "zip",
"address.countryCode": "countryCode",
phone: "phone",
},
};
// Merchant → primary (PATCH body)
const body = buildContactUpdateFromMerchant({
externalUserId: user.id,
kind: "address",
merchant: user,
mapping,
});
// Primary webhook → merchant partial
const patch = mapContactEventToMerchant(event, mapping);When a rename map is not enough (for example splitting or joining a full name), pass set and get on the same ContactInfoMapping. Rename mappings still run first; function results are merged on top.
const mapping: ContactInfoMapping<MerchantContact> = {
rename: { phone: "phone", "address.line1": "street" /* … */ },
set: (merchant) => {
const [firstName, ...rest] = merchant.fullName.trim().split(/\s+/);
return {
firstName: firstName || undefined,
lastName: rest.length > 0 ? rest.join(" ") : undefined,
};
},
get: (primary) => ({
fullName: [primary.firstName, primary.lastName].filter(Boolean).join(" "),
}),
};
await client.saveMerchantContactInfo({
externalUserId: user.id,
kind: "name",
merchant: user,
mapping,
});For address updates that change only some lines, pass previousMerchant to buildContactUpdateFromMerchant (or saveMerchantContactInfo) so primary can fuzzy-match the saved address being updated.