cimplify
TypeScript SDK

Link

client.link covers saved addresses, saved mobile money, link preferences, and sessions. Customer-scoped surface routed through the SDK's separate linkApiUrl transport, not the per-business storefront API.

Cimplify Link is the customer-scoped, cross-business product. The SDK ships a typed client.link service that talks to a different host than the rest of the storefront API.

Topology

@cimplify/sdk carries two base URLs:

  • baseUrl: the per-business storefront API (storefronts.cimplify.io in prod, :8082 in dev).
  • linkApiUrl: the customer-scoped Link API (api.cimplify.io in prod, :8080 in dev).

Both default sensibly; override either via the client constructor:

import { createCimplifyClient } from '@cimplify/sdk'

const client = createCimplifyClient({
  publicKey: 'cpk_test_…',
  // Optional overrides:
  // baseUrl:    'https://storefronts.cimplify.io',
  // linkApiUrl: 'https://api.cimplify.io',
})

Every client.link.* call goes through linkApiUrl. Everything else goes through baseUrl.

Authentication

Link is customer-bound, not business-bound. Direct client.link.* integrations authenticate with the Link API OTP flow and receive a bearer session_token. Checkout authenticates through Storefront OAuth; see CimplifyCheckout for that path.

await client.link.requestOtp({ contact: '+233244000000', contact_type: 'phone' });
const auth = await client.link.verifyOtp({
  contact: '+233244000000',
  contact_type: 'phone',
  otp_code: '123456',
});
if (!auth.ok) throw auth.error;

// session_token is wired into the client automatically.
auth.value.session_token;
auth.value.refresh_token;
auth.value.customer_id;

AuthResponse:

{
  success: boolean;
  session_token: string | null;
  refresh_token?: string | null;
  account_id?: string | null;
  customer_id: string | null;
  name?: string | null;
  email?: string | null;
  phone?: string | null;
  message: string;
}

To restore a Link session, call refreshSession. It accepts an explicit refresh token for server-side callers. Browser callers on Cimplify-owned domains can call it without an argument and rely on the HttpOnly cim_session cookie.

const refreshed = await client.link.refreshSession(auth.value.refresh_token ?? undefined);
if (refreshed.ok) {
  refreshed.value.session_token; // also installed on the client
}

The all-in-one read

For most surfaces (dashboards, checkout pre-fill) you want one round trip that returns everything:

const data = await client.link.getLinkData()
if (!data.ok) throw data.error

const { customer, addresses, mobile_money, preferences,
        default_address, default_mobile_money } = data.value

That single call replaces what would otherwise be four separate fetches (profile + addresses + mobile money + preferences).

Addresses

Inputs use the wire shape: write with the same field names you read from responses.

// Create
const created = await client.link.createAddress({
  label: 'Home',
  street_address: '12 Independence Ave',
  apartment: 'Flat 3',
  city: 'Accra',
  region: 'Greater Accra',
  country: 'GH',
})
if (!created.ok) throw created.error

// Update: partial body, POST not PATCH (matches the production wire)
await client.link.updateAddress({
  address_id: created.value.id,
  label: 'Office',
})

// Default + usage tracking
await client.link.setDefaultAddress(created.value.id)
await client.link.trackAddressUsage(created.value.id)

// Delete
await client.link.deleteAddress(created.value.id)

The full CustomerAddress shape:

{
  id: string
  customer_id: string
  label: string
  street_address: string
  apartment: string | null
  city: string
  region: string
  postal_code: string | null
  country: string | null
  delivery_instructions: string | null
  phone_for_delivery: string | null
  latitude: number | null
  longitude: number | null
  is_default: boolean | null
  usage_count: number | null
  last_used_at: string | null
  created_at: string
  updated_at: string
}

Mobile money

provider is a typed enum (MobileMoneyProvider). The backend's normalize_mobile_money_provider accepts these values plus supported aliases, then maps to the short codes the payment rail expects on the wire.

import type { MobileMoneyProvider } from '@cimplify/sdk'

// Canonical values: 'mtn' | 'vodafone' | 'telecel' | 'airtel' | 'airteltigo' | 'mpesa'
const mm = await client.link.createMobileMoney({
  phone_number: '+233244000000',
  provider: 'telecel',  // rebranded Vodafone Ghana
  label: 'My main account',
})
if (!mm.ok) throw mm.error

await client.link.verifyMobileMoney(mm.value.id)
await client.link.setDefaultMobileMoney(mm.value.id)
await client.link.trackMobileMoneyUsage(mm.value.id)
await client.link.deleteMobileMoney(mm.value.id)

Express checkout

Once a customer has saved addresses and mobile money via Link, the storefront checkout call can reference them by id instead of re-collecting:

await client.checkout.process({
  cart_id: cart.id,
  customer: { name, email, phone },
  order_type: 'delivery',
  payment_method: 'mobile_money',
  link_address_id:        savedAddress.id,
  link_payment_method_id: savedMobileMoney.id,
})

The lens resolves these server-side via the Link service before constructing the order.

Enrollment

A customer becomes a Link customer by enrolling. The atomic helper attaches the act of enrolling to an existing order in one transaction:

// Standalone enrollment
await client.link.enroll({ contact: '+233244000000', name: 'Jane Doe' })

// Or attach an order at the same time
await client.link.enrollAndLinkOrder({
  order_id: 'ord_…',
  business_id: 'bus_…',
  address:      { label: 'Home', street_address: '…', city: 'Accra', region: 'GA' },
  mobile_money: { phone_number: '+233244000000', provider: 'mtn', label: 'My MTN' },
  order_type: 'delivery',
})

The atomic variant avoids the race where you'd enroll, then attach an order, and have something fail in the middle.

Preferences

const prefs = await client.link.getPreferences()
if (!prefs.ok) throw prefs.error

await client.link.updatePreferences({
  preferred_order_type: 'delivery',
  notify_on_order: true,
  notify_on_payment: false,
  default_address_id: savedAddress.id,
})

Throws NOT_ENROLLED if the customer hasn't enrolled yet.

Sessions

Link is multi-device; revoke specific sessions or all sessions at once.

const sessions = await client.link.getSessions()
if (!sessions.ok) throw sessions.error

await client.link.revokeSession(sessions.value[0].id)
await client.link.revokeAllSessions()

Method reference

MethodReturns
getLinkData()Result<LinkData>
requestOtp(input)Result<SuccessResult>
verifyOtp(input)Result<AuthResponse>
refreshSession(sessionToken?)Result<AuthResponse>
logout()Result<SuccessResult>
checkStatus(contact)Result<LinkStatusResult>
updateProfile(input)Result<Customer>
enroll(data, opts?)Result<LinkEnrollResult>
enrollAndLinkOrder(input, opts?)Result<EnrollAndLinkOrderResult>
getOrders(options?)Result<Order[]>
getOrder(orderId, options?)Result<Order>
getPreferences()Result<CustomerLinkPreferences>
updatePreferences(patch)Result<SuccessResult>
getAddresses()Result<CustomerAddress[]>
createAddress(input, opts?)Result<CustomerAddress>
updateAddress(input)Result<SuccessResult>
deleteAddress(id)Result<SuccessResult>
setDefaultAddress(id)Result<SuccessResult>
trackAddressUsage(id)Result<SuccessResult>
getMobileMoney()Result<CustomerMobileMoney[]>
createMobileMoney(input, opts?)Result<CustomerMobileMoney>
deleteMobileMoney(id)Result<SuccessResult>
setDefaultMobileMoney(id)Result<SuccessResult>
trackMobileMoneyUsage(id)Result<SuccessResult>
verifyMobileMoney(id)Result<SuccessResult>
getSessions()Result<LinkSession[]>
revokeSession(id)Result<RevokeSessionResult>
revokeAllSessions()Result<RevokeAllSessionsResult>

Mock parity

The in-process mock (@cimplify/sdk/mock and createTestClient) implements every Link route. Storefront agents can write Link UX with client.link.* and test it offline; no real linkApiUrl needed.

import { createTestClient } from '@cimplify/sdk/testing'

const h = createTestClient({ seed: 'retail' })
await h.client.link.requestOtp({ contact: '+233244000000', contact_type: 'phone' })
await h.client.link.verifyOtp({
  contact: '+233244000000',
  contact_type: 'phone',
  otp_code: '123456',
})

const data = await h.client.link.getLinkData()
// data.value.customer, .addresses, .mobile_money, .preferences …

See Testing harness: createTestClient.

On this page