@openmerge/core
v0.2.1
Published
Server-side TypeScript client for the OpenMerge Core API
Readme
@openmerge/core
Typed, server-only Node.js 20+ client for the OpenMerge Core API. This package is intentionally separate from @openmerge/sdk (IR authoring) and @openmerge/js (browser widgets).
Install
pnpm add @openmerge/coreKeep the workspace API key in the server environment. @openmerge/core rejects construction in browser runtimes; only short-lived link tokens may cross the server/browser boundary.
import { OpenMerge } from "@openmerge/core"
const openmerge = new OpenMerge({
apiKey: process.env.OPENMERGE_API_KEY!,
baseUrl: process.env.OPENMERGE_API_URL ?? "https://api.openmerge.dev",
})Create a customer link session
Run this code in your backend route. Return the link token or hosted URL—not the API key—to your browser application.
const link = await openmerge.linkTokens.create({
workspaceId: "ws_123",
endUserOriginId: "customer_456",
allowedCategories: ["crm"],
hostOrigin: "https://app.customer.example",
})
return Response.json({ token: link.token, hostedUrl: link.hostedUrl })Reconnect preserves the linked account's provider and OAuth-app affinity:
const reconnect = await openmerge.linkTokens.reconnect({
workspaceId: "ws_123",
linkedAccountId: "la_123",
hostOrigin: "https://app.customer.example",
})Linked accounts and unified records
const accounts = await openmerge.linkedAccounts.list("ws_123")
const account = await openmerge.linkedAccounts.get(accounts[0].id)
for await (const contact of openmerge.unifiedRecords.iterate<{ email?: string }>("Contact", {
workspaceId: "ws_123",
linkedAccountId: account.id,
pageSize: 200,
includeRemoteData: false,
includeDeleted: false,
})) {
console.log(contact.unified_id, contact.data.email)
}Use listPage() when the application owns cursor persistence:
const page = await openmerge.unifiedRecords.listPage("Contact", {
workspaceId: "ws_123",
linkedAccountId: "la_123",
cursor: savedCursor,
pageSize: 100,
})
await saveCursor(page.nextCursor)Idempotent writeback
Every write requires an application-owned idempotency key. Replaying the same key and payload returns the same operation; reusing it with different changes returns a typed ConflictError.
import { ConflictError } from "@openmerge/core"
try {
const write = await openmerge.writebacks.submit({
workspaceId: "ws_123",
linkedAccountId: "la_123",
model: "Contact",
unifiedId: "contact_123",
changes: { email: "[email protected]" },
idempotencyKey: "crm-update:event_01JXYZ",
})
const terminal = await openmerge.writebacks.waitForTerminal(write.id, "ws_123", {
timeoutMs: 120_000,
})
console.log(terminal.state, terminal.result)
} catch (error) {
if (error instanceof ConflictError) {
console.error(error.requestId, error.message)
}
throw error
}Retries and request correlation
The client retries network errors and HTTP 408, 425, 429, 500, 502, 503, and 504 with bounded exponential backoff. Retries apply only to safe reads or requests carrying an explicit Idempotency-Key; unsafe lifecycle operations are never silently replayed. The same X-Request-ID is retained across attempts and all ApiError instances expose the server request ID, status, retryability, and structured details.
const openmerge = new OpenMerge({
apiKey: process.env.OPENMERGE_API_KEY!,
baseUrl: "https://api.openmerge.dev",
timeoutMs: 30_000,
maxRetries: 2,
maxRetryDelayMs: 10_000,
})Dynamic connector and model catalog
Do not hardcode providers, models, or writable fields. The catalog is workspace-aware and comes from installed connector bundles:
const [integrations, models] = await Promise.all([
openmerge.integrations.list("ws_123"),
openmerge.models.list("ws_123"),
])
const contact = models.find((model) => model.id === "Contact")
const hubspotWritable = contact?.base_two_way_field_ids_by_provider?.hubspot ?? []Verify webhooks
Verify the exact raw request body before JSON parsing. The helper enforces the OpenMerge-Signature timestamp tolerance and uses constant-time HMAC comparison:
import { verifyWebhookSignature } from "@openmerge/core"
const rawBody = new Uint8Array(await request.arrayBuffer())
verifyWebhookSignature(rawBody, request.headers.get("OpenMerge-Signature") ?? "", process.env.OPENMERGE_WEBHOOK_SECRET!)
const event = JSON.parse(new TextDecoder().decode(rawBody))