Skip to Content
The AKIN Travel developer platform is in beta — APIs may change. Get started →
Server SDK (Node)

Server SDK (Node)

The @akin-travel/partner-sdk/server entry is a typed Node client for the server-to-server API. It wraps the same operations you can call with raw GraphQL — reads and enrollment — but gives you a typed method per operation, structured errors, and first-class rate-limit handling, so your backend doesn’t hand-roll fetch + query strings.

Server only. This entry sends your secret x-partner-api-key — a server-only key, never the publishable (browser-safe) one. It pulls in no React, Firebase, Apollo, or browser code, and it refuses to construct in a browser. Never import it into client-side code — use the React entry (@akin-travel/partner-sdk) there. Writes need the matching scope on the key (member:write to enrol, loyalty:write to earn/burn); see publishable vs secret keys.

It’s also the recommended answer for React Native: your backend uses this SDK, and your mobile client renders the loyalty state your backend caches — no client SDK required.

Install

npm install @akin-travel/partner-sdk

The server client is published under the /server subpath of the existing package — there is no separate install.

Create a client

import { AkinServerClient } from '@akin-travel/partner-sdk/server'; const akin = new AkinServerClient({ apiKey: process.env.AKIN_PARTNER_API_KEY!, // pk_test_* / pk_live_* environment: 'production', // 'production' | 'staging' | 'development' });
OptionDefaultNotes
apiKey— (required)Your partner key. Keep it on the server.
environment'production'Selects the default endpoint.
baseUrlper-environmentExplicit GraphQL endpoint — overrides environment.
partnerIdSent as x-partner-id; the API rejects a mismatched key.
fetchglobal fetchInject a custom fetch (proxy/agent or tests).
timeoutMsAbort + reject the request after this long.

The partner is always derived from the key — there is no partnerId argument on any read or mutation to pass (or spoof).

Reads

memberByEmail

Resolve a member and their current tier + points with your partner:

import { AkinMemberNotFoundError } from '@akin-travel/partner-sdk/server'; try { const member = await akin.memberByEmail('guest@example.com'); console.log(member.memberId, member.tier, member.points); } catch (err) { if (err instanceof AkinMemberNotFoundError) { // Not one of your enrolled members (indistinguishable from "no such member" // by design — see /server-to-server). Branch on the class, never the message. } }

loyaltyTransactionsWithMetrics

Read a member’s loyalty ledger with aggregated stay/spend metrics. Scoped to your partner: the member must be enrolled with your partner (an unenrolled or unknown member raises AkinMemberNotFoundError, indistinguishable by design), and the *Partner metric columns are always computed against your partner — the partnerId option can’t widen that scope:

const { items, totalCount } = await akin.loyaltyTransactionsWithMetrics({ memberId: member.memberId, limit: 20, offset: 0, });

tierConfigs

Read your partner’s tier ladder (for rendering your own tier UI):

const tiers = await akin.tierConfigs();

Enrollment

enrollMember

Enrol a member by email + profile under your partner. Idempotent on (your partner, email) — safe to call on every registration and retry:

const { memberId, created, enrollmentChanged } = await akin.enrollMember({ email: 'ada@example.com', firstName: 'Ada', lastName: 'Lovelace', externalId: 'your-loyalty-id-123', // optional; your own key for this member });

created is true only when a new AKIN account was made; enrollmentChanged is true only when this enrolment was new (vs an idempotent replay). See Server-side enrollment for the full semantics.

Errors

Every failure is a typed subclass of AkinServerError. Branch on the class (or error.code), never the message.

ClassWhen
AkinMemberNotFoundErrorA lookup didn’t resolve to one of your members (code: MEMBER_NOT_FOUND).
AkinRateLimitErrorYou were rate-limited. Carries retryAfterSeconds + parsed rateLimit headers.
AkinApiErrorAny other GraphQL error or non-2xx response (code, graphQLErrors, statusCode).
AkinRequestErrorTransport failure — the request never produced a response (DNS, refused, timeout).

Respecting rate limits

The SDK surfaces the API’s rate limits as a typed error so you own the back-off:

import { AkinRateLimitError } from '@akin-travel/partner-sdk/server'; try { await akin.memberByEmail(email); } catch (err) { if (err instanceof AkinRateLimitError) { const wait = err.retryAfterSeconds ?? 1; await new Promise((r) => setTimeout(r, wait * 1000)); // ...then retry } }

AkinRateLimitError is raised for both shapes the API can return — the GraphQL RATE_LIMITED extension and the HTTP-429 middleware body — and resolves retryAfterSeconds from the error body or the Retry-After header. Read err.rateLimit (limit / remaining / reset) to throttle before you hit the wall.

Types

Every method is typed end-to-end — results are inferred from the same generated operations the rest of the platform uses, so they can’t drift from the API. The public result and option types (PartnerMember, EnrollMemberInput, EnrollMemberResult, TierConfig, LoyaltyTransaction, …) are exported from @akin-travel/partner-sdk/server.