@occassionly/partner-sdk 0.1.0-ftpartner.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # @occassionly/partner-sdk
2
+
3
+ Official SDK for the Occassionly Partner API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i @occassionly/partner-sdk
9
+ ```
10
+
11
+ For builds published automatically from branch `ft/partner`:
12
+
13
+ ```bash
14
+ npm i @occassionly/partner-sdk@ft-partner
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { createClient } from "@occassionly/partner-sdk";
21
+
22
+ const client = createClient({
23
+ baseUrl: "https://api.occassionly.com/api",
24
+ partnerId: process.env.OCC_PARTNER_ID!,
25
+ apiKeyId: process.env.OCC_API_KEY_ID!,
26
+ apiSecret: process.env.OCC_API_SECRET!,
27
+ });
28
+
29
+ const credits = await client.getCredits();
30
+ ```
31
+
32
+ ## Methods
33
+
34
+ - `getCredits()`
35
+ - `createEvent(payload)`
36
+ - `issueCard(payload)`
37
+ - `issueCardsBulk(payload)`
38
+ - `revokeCard(occCardId, payload?)`
39
+ - `verify(payload)`
40
+ - `listWebhookOutbox(params?)`
41
+ - `retryDueWebhooks()`
42
+ - `replayWebhook(webhookEventId)`
43
+
44
+ ## Payloads and sample responses
45
+
46
+ ### `getCredits()`
47
+
48
+ Response:
49
+
50
+ ```json
51
+ {
52
+ "partner_id": "partner_demo",
53
+ "credits_total": 500,
54
+ "credits_used": 120,
55
+ "credits_remaining": 380
56
+ }
57
+ ```
58
+
59
+ ### `createEvent(payload)`
60
+
61
+ Payload:
62
+
63
+ ```json
64
+ {
65
+ "partner_event_id": "evt_12345",
66
+ "event_name": "Wedding Reception",
67
+ "start_at": "2026-03-28T13:00:00.000Z",
68
+ "end_at": "2026-03-28T18:00:00.000Z",
69
+ "timezone": "Africa/Lagos",
70
+ "template": { "theme": "classic" }
71
+ }
72
+ ```
73
+
74
+ Response:
75
+
76
+ ```json
77
+ {
78
+ "occ_event_id": "67f0f7c2e7f8f92b4d0e0c11",
79
+ "status": "active"
80
+ }
81
+ ```
82
+
83
+ ### `issueCard(payload)`
84
+
85
+ Payload:
86
+
87
+ ```json
88
+ {
89
+ "issue_ref": "evt_12345_guest_001",
90
+ "partner_event_id": "evt_12345",
91
+ "partner_guest_id": "guest_001",
92
+ "guest_name": "Ibrahim Musa",
93
+ "email": "ibrahim@example.com"
94
+ }
95
+ ```
96
+
97
+ Response:
98
+
99
+ ```json
100
+ {
101
+ "occ_card_id": "occ_card_ab12cd34",
102
+ "status": "issued",
103
+ "card_url": "https://occassionly.com/c/occ_card_ab12cd34",
104
+ "qr_payload": "OCCP|occ_card_ab12cd34|ea73..."
105
+ }
106
+ ```
107
+
108
+ ### `issueCardsBulk(payload)`
109
+
110
+ Payload:
111
+
112
+ ```json
113
+ {
114
+ "items": [
115
+ {
116
+ "issue_ref": "evt_12345_guest_001",
117
+ "partner_event_id": "evt_12345",
118
+ "partner_guest_id": "guest_001",
119
+ "guest_name": "Ibrahim Musa"
120
+ },
121
+ {
122
+ "issue_ref": "evt_12345_guest_002",
123
+ "partner_event_id": "evt_12345",
124
+ "partner_guest_id": "guest_002",
125
+ "guest_name": "Aisha Bello"
126
+ }
127
+ ]
128
+ }
129
+ ```
130
+
131
+ Response:
132
+
133
+ ```json
134
+ {
135
+ "total": 2,
136
+ "created": 2,
137
+ "idempotent": 0,
138
+ "failed": 0,
139
+ "results": []
140
+ }
141
+ ```
142
+
143
+ ### `verify(payload)`
144
+
145
+ Payload:
146
+
147
+ ```json
148
+ {
149
+ "qr_payload": "OCCP|occ_card_ab12cd34|ea73..."
150
+ }
151
+ ```
152
+
153
+ Response:
154
+
155
+ ```json
156
+ {
157
+ "status": "valid",
158
+ "occ_card_id": "occ_card_ab12cd34"
159
+ }
160
+ ```
161
+
162
+ Other `status` values: `invalid`, `already_verified`, `revoked`.
163
+
164
+ ### `revokeCard(occCardId, payload?)`
165
+
166
+ Payload:
167
+
168
+ ```json
169
+ {
170
+ "reason": "manual_revoke"
171
+ }
172
+ ```
173
+
174
+ Response:
175
+
176
+ ```json
177
+ {
178
+ "occ_card_id": "occ_card_ab12cd34",
179
+ "status": "revoked"
180
+ }
181
+ ```
182
+
183
+ ### `listWebhookOutbox(params?)`
184
+
185
+ Query params:
186
+ - `status`: `pending | failed | dead_letter | delivered`
187
+ - `event_type`: string
188
+ - `limit`: number
189
+ - `cursor`: string
190
+
191
+ Response:
192
+
193
+ ```json
194
+ {
195
+ "items": [
196
+ {
197
+ "webhook_event_id": "4f9a...",
198
+ "event_type": "card.issued",
199
+ "status": "delivered"
200
+ }
201
+ ],
202
+ "next_cursor": null
203
+ }
204
+ ```
205
+
206
+ ### `retryDueWebhooks()`
207
+
208
+ Response:
209
+
210
+ ```json
211
+ {
212
+ "scanned": 10,
213
+ "retried": 3
214
+ }
215
+ ```
216
+
217
+ ### `replayWebhook(webhookEventId)`
218
+
219
+ Response:
220
+
221
+ ```json
222
+ {
223
+ "replayed": true
224
+ }
225
+ ```
226
+
227
+ ## Common error response
228
+
229
+ ```json
230
+ {
231
+ "success": false,
232
+ "error": {
233
+ "code": "INSUFFICIENT_CREDITS",
234
+ "message": "Partner credit balance exhausted.",
235
+ "details": null
236
+ }
237
+ }
238
+ ```
@@ -0,0 +1,95 @@
1
+ type HttpMethod = "GET" | "POST";
2
+ export type PartnerClientOptions = {
3
+ baseUrl: string;
4
+ partnerId: string;
5
+ apiKeyId: string;
6
+ apiSecret: string;
7
+ fetchImpl?: typeof fetch;
8
+ };
9
+ export declare class PartnerApiError extends Error {
10
+ status: number;
11
+ code?: string;
12
+ details?: unknown;
13
+ constructor(message: string, status: number, code?: string, details?: unknown);
14
+ }
15
+ export declare function createClient(options: PartnerClientOptions): {
16
+ request: <T>(method: HttpMethod, path: string, body?: unknown, query?: Record<string, string | number | boolean | undefined>) => Promise<T>;
17
+ getCredits: () => Promise<{
18
+ partner_id: string;
19
+ credits_total: number;
20
+ credits_used: number;
21
+ credits_remaining: number;
22
+ }>;
23
+ createEvent: (payload: {
24
+ partner_event_id: string;
25
+ event_name: string;
26
+ start_at?: string;
27
+ end_at?: string;
28
+ timezone?: string;
29
+ template?: Record<string, unknown>;
30
+ }) => Promise<{
31
+ occ_event_id: string;
32
+ status: string;
33
+ }>;
34
+ issueCard: (payload: {
35
+ issue_ref: string;
36
+ partner_event_id: string;
37
+ partner_guest_id: string;
38
+ guest_name: string;
39
+ email?: string;
40
+ phone?: string;
41
+ metadata?: Record<string, unknown>;
42
+ }) => Promise<{
43
+ occ_card_id: string;
44
+ status: string;
45
+ card_url: string;
46
+ qr_payload: string;
47
+ }>;
48
+ issueCardsBulk: (payload: {
49
+ items: Array<{
50
+ issue_ref: string;
51
+ partner_event_id: string;
52
+ partner_guest_id: string;
53
+ guest_name: string;
54
+ email?: string;
55
+ phone?: string;
56
+ metadata?: Record<string, unknown>;
57
+ }>;
58
+ }) => Promise<{
59
+ total: number;
60
+ created: number;
61
+ idempotent: number;
62
+ failed: number;
63
+ results: Array<Record<string, unknown>>;
64
+ }>;
65
+ revokeCard: (occCardId: string, payload?: {
66
+ reason?: string;
67
+ }) => Promise<{
68
+ occ_card_id: string;
69
+ status: string;
70
+ }>;
71
+ verify: (payload: {
72
+ qr_payload: string;
73
+ metadata?: Record<string, unknown>;
74
+ }) => Promise<{
75
+ status: "valid" | "invalid" | "already_verified" | "revoked";
76
+ occ_card_id?: string;
77
+ }>;
78
+ listWebhookOutbox: (params?: {
79
+ status?: "pending" | "failed" | "dead_letter" | "delivered";
80
+ event_type?: string;
81
+ limit?: number;
82
+ cursor?: string;
83
+ }) => Promise<{
84
+ items: Array<Record<string, unknown>>;
85
+ next_cursor: string | null;
86
+ }>;
87
+ retryDueWebhooks: () => Promise<{
88
+ scanned: number;
89
+ retried: number;
90
+ }>;
91
+ replayWebhook: (webhookEventId: string) => Promise<{
92
+ replayed: boolean;
93
+ }>;
94
+ };
95
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,89 @@
1
+ import { createHmac } from "crypto";
2
+ export class PartnerApiError extends Error {
3
+ status;
4
+ code;
5
+ details;
6
+ constructor(message, status, code, details) {
7
+ super(message);
8
+ this.name = "PartnerApiError";
9
+ this.status = status;
10
+ this.code = code;
11
+ this.details = details;
12
+ }
13
+ }
14
+ const normalizeBaseUrl = (baseUrl) => baseUrl.replace(/\/+$/, "");
15
+ const normalizePath = (path) => {
16
+ const trimmed = path.trim();
17
+ if (!trimmed)
18
+ throw new Error("Path cannot be empty");
19
+ if (trimmed.startsWith("/api/v1/partner/"))
20
+ return trimmed;
21
+ if (trimmed.startsWith("/v1/partner/"))
22
+ return `/api${trimmed}`;
23
+ if (trimmed.startsWith("/"))
24
+ return `/api${trimmed}`;
25
+ return `/api/${trimmed}`;
26
+ };
27
+ const stableStringify = (value) => {
28
+ if (value == null)
29
+ return "";
30
+ return JSON.stringify(value);
31
+ };
32
+ const sign = (secret, canonical) => createHmac("sha256", secret).update(canonical, "utf8").digest("hex");
33
+ const buildHeaders = (input) => ({
34
+ "Content-Type": "application/json",
35
+ "x-partner-id": input.partnerId,
36
+ "x-api-key": input.apiKeyId,
37
+ "x-timestamp": input.timestamp,
38
+ "x-signature": input.signature,
39
+ });
40
+ async function handleResponse(res) {
41
+ const payload = (await res.json().catch(() => ({})));
42
+ if (!res.ok || !payload || payload.success === false) {
43
+ const failure = payload;
44
+ throw new PartnerApiError(failure?.error?.message || `Partner API request failed (${res.status})`, res.status, failure?.error?.code, failure?.error?.details);
45
+ }
46
+ return payload.data;
47
+ }
48
+ export function createClient(options) {
49
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
50
+ const fetchImpl = options.fetchImpl ?? fetch;
51
+ const request = async (method, path, body, query) => {
52
+ const canonicalPath = normalizePath(path);
53
+ const bodyText = method === "POST" ? stableStringify(body) : "";
54
+ const timestamp = Date.now().toString();
55
+ const canonical = `${timestamp}${method}${canonicalPath}${bodyText}`;
56
+ const signature = sign(options.apiSecret, canonical);
57
+ const url = new URL(`${baseUrl}${canonicalPath}`);
58
+ if (query) {
59
+ for (const [key, value] of Object.entries(query)) {
60
+ if (value !== undefined) {
61
+ url.searchParams.set(key, String(value));
62
+ }
63
+ }
64
+ }
65
+ const res = await fetchImpl(url.toString(), {
66
+ method,
67
+ headers: buildHeaders({
68
+ partnerId: options.partnerId,
69
+ apiKeyId: options.apiKeyId,
70
+ signature,
71
+ timestamp,
72
+ }),
73
+ body: method === "POST" ? bodyText : undefined,
74
+ });
75
+ return handleResponse(res);
76
+ };
77
+ return {
78
+ request,
79
+ getCredits: () => request("GET", "/v1/partner/credits"),
80
+ createEvent: (payload) => request("POST", "/v1/partner/events", payload),
81
+ issueCard: (payload) => request("POST", "/v1/partner/cards", payload),
82
+ issueCardsBulk: (payload) => request("POST", "/v1/partner/cards/bulk", payload),
83
+ revokeCard: (occCardId, payload) => request("POST", `/v1/partner/cards/${encodeURIComponent(occCardId)}/revoke`, payload || {}),
84
+ verify: (payload) => request("POST", "/v1/partner/verify", payload),
85
+ listWebhookOutbox: (params) => request("GET", "/v1/partner/webhooks/outbox", undefined, params),
86
+ retryDueWebhooks: () => request("POST", "/v1/partner/webhooks/outbox/retry-due", {}),
87
+ replayWebhook: (webhookEventId) => request("POST", `/v1/partner/webhooks/outbox/${encodeURIComponent(webhookEventId)}/replay`, {}),
88
+ };
89
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@occassionly/partner-sdk",
3
+ "version": "0.1.0-ftpartner.4",
4
+ "description": "Official Occassionly Partner API SDK",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20.19.33",
17
+ "typescript": "^5.9.3"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ }
22
+ }