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: 'pk_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. To use any read or write method, the SDK needs an authenticated customer session. Request and verify an OTP first:

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
// auth.value.session_token is now wired into the client automatically.

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: same field names you read off the response. There is no translation layer.

// 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 (plus legacy spellings) and maps to the short codes Paystack 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>
logout()Result<SuccessResult>
checkStatus(contact)Result<LinkStatusResult>
enroll(data, opts?)Result<LinkEnrollResult>
enrollAndLinkOrder(input, opts?)Result<EnrollAndLinkOrderResult>
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.auth.requestOtp('+233244000000')
await h.client.auth.verifyOtp('123456', '+233244000000')

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

See Testing harness: createTestClient.

On this page