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.ioin prod,:8082in dev).linkApiUrl: the customer-scoped Link API (api.cimplify.ioin prod,:8080in 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.valueThat 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
| Method | Returns |
|---|---|
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.
Related
- Cimplify Link product overview: the iframe Elements (auth, checkout, account)
- API reference: /v1/link
- Concepts: Idempotency
Support
Customer-side chat conversation. The server resolves the active conversation from the widget identity on every request; there is no `conversation_id` to thread through. Open, send, and poll.
Errors
Every SDK method returns `Result<T, CimplifyError>`. Methods do not throw; instead you check `.ok` and branch on `error.code`. Wrap nothing in `try/catch`.