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:writeto enrol,loyalty:writeto 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-sdkThe 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'
});| Option | Default | Notes |
|---|---|---|
apiKey | — (required) | Your partner key. Keep it on the server. |
environment | 'production' | Selects the default endpoint. |
baseUrl | per-environment | Explicit GraphQL endpoint — overrides environment. |
partnerId | — | Sent as x-partner-id; the API rejects a mismatched key. |
fetch | global fetch | Inject a custom fetch (proxy/agent or tests). |
timeoutMs | — | Abort + 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.
| Class | When |
|---|---|
AkinMemberNotFoundError | A lookup didn’t resolve to one of your members (code: MEMBER_NOT_FOUND). |
AkinRateLimitError | You were rate-limited. Carries retryAfterSeconds + parsed rateLimit headers. |
AkinApiError | Any other GraphQL error or non-2xx response (code, graphQLErrors, statusCode). |
AkinRequestError | Transport 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.