Developers
API reference
Endpoint
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 tokens
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.
Authentication
Requests use Authorization: Bearer <api-key> and Content-Type: application/json. An optional idempotency-key header is supported for safe retries.
Request body (summary)
externalUserId— the user in your app (never an Anemone-only id)kind— e.g.addressaddress— optionalline1,line2,city,region,postalCode,countryCode,label
For full types, import from anemone-server-js in your TypeScript project; source lives in the anemone-server-js package. Partner-side push example: pushContactUpdatesToPrimary.
Deferred update queue
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).
Contact field mapping
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.
Rename map
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);Custom set / get functions
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.