@thisispamela/sdk 1.0.0 → 1.0.1

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 CHANGED
@@ -1,11 +1,11 @@
1
1
  # Pamela SDK for JavaScript/TypeScript
2
2
 
3
- Official SDK for the Pamela Voice API.
3
+ Official SDK for the Pamela Enterprise Voice API.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @pamela/sdk
8
+ npm install @thisispamela/sdk
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -13,7 +13,7 @@ npm install @pamela/sdk
13
13
  ### Basic Example
14
14
 
15
15
  ```typescript
16
- import { PamelaClient } from '@pamela/sdk';
16
+ import { PamelaClient } from '@thisispamela/sdk';
17
17
 
18
18
  const client = new PamelaClient({
19
19
  apiKey: 'pk_live_your_api_key_here',
@@ -37,7 +37,7 @@ console.log('Call status:', status.status);
37
37
  ### Webhook Verification
38
38
 
39
39
  ```typescript
40
- import { PamelaClient } from '@pamela/b2b-sdk';
40
+ import { PamelaClient } from '@thisispamela/sdk';
41
41
  import express from 'express';
42
42
 
43
43
  const app = express();
@@ -59,7 +59,154 @@ app.post('/webhooks/pamela', express.json(), (req, res) => {
59
59
  });
60
60
  ```
61
61
 
62
+ ## Getting API Keys
63
+
64
+ ### Obtaining Your API Key
65
+
66
+ API keys are created and managed through the Pamela Partner Portal or via the Partner API:
67
+
68
+ 1. **Sign up for an Enterprise subscription** (see Subscription Requirements below)
69
+ 2. **Create an API key** via one of these methods:
70
+ - Partner Portal: Log in to your account and navigate to the Enterprise panel
71
+ - Partner API: `POST /api/b2b/v1/partner/api-keys`
72
+ ```bash
73
+ curl -X POST https://api.thisispamela.com/api/b2b/v1/partner/api-keys \
74
+ -H "Authorization: Bearer YOUR_B2C_USER_TOKEN" \
75
+ -H "Content-Type: application/json" \
76
+ -d '{"project_id": "optional-project-id", "key_prefix": "pk_live_"}'
77
+ ```
78
+ 3. **Save your API key immediately** - the full key is only returned once during creation
79
+ 4. **Use the key prefix** (`pk_live_` or `pk_test_`) to identify keys in your account
80
+
81
+ ### Managing API Keys
82
+
83
+ - **List API keys**: `GET /api/b2b/v1/partner/api-keys`
84
+ - **Revoke API key**: `POST /api/b2b/v1/partner/api-keys/{key_id}/revoke`
85
+ - **Associate with projects**: Optionally link API keys to specific projects for better organization
86
+
87
+ ### API Key Format
88
+
89
+ - **Live keys**: Start with `pk_live_` (for production use)
90
+ - **Test keys**: Start with `pk_test_` (for development/testing)
91
+ - **Security**: Keys are hashed in the database. Store them securely and never commit them to version control.
92
+
93
+ ## Subscription Requirements
94
+
95
+ ### Enterprise Subscription Required
96
+
97
+ **All API access requires an active Enterprise subscription.** API calls will return `403 Forbidden` if:
98
+ - No Enterprise subscription is active
99
+ - Subscription status is `past_due` and grace period has expired
100
+ - Subscription status is `canceled`
101
+
102
+ ### Grace Period
103
+
104
+ Enterprise subscriptions have a **1-week grace period** when payment fails:
105
+ - During grace period: API access is allowed, but usage is still charged
106
+ - After grace period expires: API access is blocked until payment is updated
107
+
108
+ ### Subscription Status Endpoints
109
+
110
+ Check subscription status using the Enterprise Partner API:
111
+ - `GET /api/b2b/v1/partner/subscription` - Get subscription status
112
+ - `POST /api/b2b/v1/partner/subscription/checkout` - Create checkout session
113
+ - `POST /api/b2b/v1/partner/subscription/portal` - Access Customer Portal
114
+
115
+ ## Error Codes
116
+
117
+ ### Authentication Errors
118
+
119
+ - `401 Unauthorized`: Invalid or missing API key
120
+ - `403 Forbidden`: Subscription inactive or expired
121
+ - Check subscription status: `subscription_status` must be `"active"`
122
+ - For `past_due`: Check `grace_period_ends_at` - access allowed during grace period
123
+
124
+ ### Validation Errors
125
+
126
+ - `400 Bad Request`: Invalid request parameters
127
+ - `404 Not Found`: Resource not found (call, partner, etc.)
128
+
129
+ ### Quota/Limit Errors
130
+
131
+ - `429 Too Many Requests`: Quota exceeded (if quota limits are set)
132
+ - Note: Enterprise subscriptions have no quota limits for API calls (all usage is billed per-minute)
133
+
134
+ ### Rate Limit Errors
135
+
136
+ - `429 Too Many Requests`: Rate limit exceeded
137
+ - Retry after the time specified in `Retry-After` header
138
+
139
+ ## Usage Limits & Billing
140
+
141
+ ### Enterprise API Usage
142
+
143
+ - **Unlimited API calls** (no call count limits)
144
+ - **All API usage billed at $0.10/minute** (10 cents per minute)
145
+ - **Minimum billing: 1 minute per call** (even if call duration < 60 seconds)
146
+ - **Billing calculation**: `billed_minutes = max(ceil(duration_seconds / 60), 1)`
147
+ - **Only calls that connect** (have `started_at`) are billed
148
+
149
+ ### Usage Tracking
150
+
151
+ - Usage is tracked in `b2b_usage` collection with `type: "api_usage"` (collection name stays `b2b_usage`)
152
+ - Usage is synced to Stripe hourly (at :00 minutes)
153
+ - Stripe meter name: `stripe_minutes`
154
+ - Failed syncs are retried with exponential backoff (1s, 2s, 4s, 8s, 16s), max 5 retries
155
+
156
+ ### Billing Period
157
+
158
+ - Billing is based on calendar months (UTC midnight on 1st of each month)
159
+ - Calls are billed in the month where `started_at` occurred
160
+ - Usage sync status: `pending`, `synced`, or `failed`
161
+
162
+ ## API Methods
163
+
164
+ ### Calls
165
+
166
+ - `createCall(request)` - Create a new call
167
+ - `getCall(callId)` - Get call status and details
168
+ - `listCalls(params?)` - List calls with optional filters
169
+ - `cancelCall(callId)` - Cancel an in-progress call
170
+
171
+ ### Tools
172
+
173
+ - `registerTool(tool)` - Register a tool
174
+ - `listTools()` - List all tools
175
+ - `deleteTool(toolId)` - Delete a tool
176
+
177
+ ### Usage
178
+
179
+ - `usage.get(period?)` - Get usage statistics
180
+
181
+ **Example:**
182
+ ```typescript
183
+ // Get current month usage
184
+ const usage = await client.usage.get();
185
+
186
+ // Get usage for specific period
187
+ const janUsage = await client.usage.get("2024-01");
188
+
189
+ console.log(`Usage: ${usage.call_count} calls, ${usage.api_minutes} minutes`);
190
+ console.log(`Quota: ${usage.quota?.partner_limit || 'Unlimited'}`);
191
+ ```
192
+
193
+ **Response:**
194
+ ```typescript
195
+ {
196
+ partner_id: "partner_123",
197
+ project_id?: "project_456",
198
+ period: "2024-01",
199
+ call_count: 150,
200
+ quota?: {
201
+ partner_limit?: number,
202
+ project_limit?: number
203
+ }
204
+ }
205
+ ```
206
+
207
+ **Note:** Enterprise subscriptions have no quota limits - all usage is billed per-minute.
208
+
62
209
  ## API Reference
63
210
 
64
- See the [Pamela B2B API Documentation](https://docs.thisispamela.com/b2b) for full API reference.
211
+ See the [Pamela Enterprise API Documentation](https://docs.thisispamela.com/enterprise) for full API reference.
65
212
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Pamela B2B Voice API SDK for JavaScript/TypeScript
2
+ * Pamela Enterprise Voice API SDK for JavaScript/TypeScript
3
3
  */
4
+ import { AxiosInstance } from 'axios';
4
5
  export interface PamelaClientConfig {
5
6
  apiKey: string;
6
7
  baseUrl?: string;
@@ -54,9 +55,28 @@ export interface WebhookPayload {
54
55
  timestamp: string;
55
56
  data: Record<string, any>;
56
57
  }
58
+ export declare class UsageClient {
59
+ private client;
60
+ constructor(client: AxiosInstance);
61
+ /**
62
+ * Get usage statistics for partner/project.
63
+ */
64
+ get(period?: string): Promise<{
65
+ partner_id: string;
66
+ project_id?: string;
67
+ period: string;
68
+ call_count: number;
69
+ last_updated?: string;
70
+ quota?: {
71
+ partner_limit?: number;
72
+ project_limit?: number;
73
+ };
74
+ }>;
75
+ }
57
76
  export declare class PamelaClient {
58
77
  private client;
59
78
  private apiKey;
79
+ usage: UsageClient;
60
80
  constructor(config: PamelaClientConfig);
61
81
  /**
62
82
  * Create a new call.
@@ -71,6 +91,7 @@ export declare class PamelaClient {
71
91
  */
72
92
  listCalls(params?: {
73
93
  status?: string;
94
+ status_filter?: string;
74
95
  limit?: number;
75
96
  offset?: number;
76
97
  start_date?: string;
@@ -128,3 +149,4 @@ export declare class PamelaClient {
128
149
  static verifyWebhookSignature(payload: string | object, signature: string, secret: string): boolean;
129
150
  }
130
151
  export default PamelaClient;
152
+ export { PamelaClient as Pamela };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Pamela B2B Voice API SDK for JavaScript/TypeScript
3
+ * Pamela Enterprise Voice API SDK for JavaScript/TypeScript
4
4
  */
5
5
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
6
  if (k2 === undefined) k2 = k;
@@ -39,9 +39,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.PamelaClient = void 0;
42
+ exports.Pamela = exports.PamelaClient = exports.UsageClient = void 0;
43
43
  const axios_1 = __importDefault(require("axios"));
44
44
  const crypto = __importStar(require("crypto"));
45
+ class UsageClient {
46
+ constructor(client) {
47
+ this.client = client;
48
+ }
49
+ /**
50
+ * Get usage statistics for partner/project.
51
+ */
52
+ async get(period) {
53
+ const params = period ? { period } : {};
54
+ const response = await this.client.get('/usage', { params });
55
+ return response.data;
56
+ }
57
+ }
58
+ exports.UsageClient = UsageClient;
45
59
  class PamelaClient {
46
60
  constructor(config) {
47
61
  this.apiKey = config.apiKey;
@@ -54,6 +68,8 @@ class PamelaClient {
54
68
  },
55
69
  timeout: 30000,
56
70
  });
71
+ // Initialize usage client
72
+ this.usage = new UsageClient(this.client);
57
73
  // Add retry logic
58
74
  this.client.interceptors.response.use((response) => response, async (error) => {
59
75
  const config = error.config;
@@ -86,7 +102,12 @@ class PamelaClient {
86
102
  * List calls for the authenticated partner/project.
87
103
  */
88
104
  async listCalls(params) {
89
- const response = await this.client.get('/calls', { params });
105
+ const normalizedParams = { ...params };
106
+ if (params?.status_filter || params?.status) {
107
+ normalizedParams.status_filter = params?.status_filter ?? params?.status;
108
+ delete normalizedParams.status;
109
+ }
110
+ const response = await this.client.get('/calls', { params: normalizedParams });
90
111
  // Backend returns { items: [...], total, limit, offset }
91
112
  return response.data;
92
113
  }
@@ -131,4 +152,6 @@ class PamelaClient {
131
152
  }
132
153
  }
133
154
  exports.PamelaClient = PamelaClient;
155
+ exports.Pamela = PamelaClient;
156
+ // Export as both default and named export for flexibility
134
157
  exports.default = PamelaClient;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thisispamela/sdk",
3
- "version": "1.0.0",
4
- "description": "Pamela B2B Voice API SDK for JavaScript/TypeScript",
3
+ "version": "1.0.1",
4
+ "description": "Pamela Enterprise Voice API SDK for JavaScript/TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
@@ -14,7 +14,7 @@
14
14
  "pamela",
15
15
  "voice",
16
16
  "api",
17
- "b2b",
17
+ "enterprise",
18
18
  "phone",
19
19
  "calling"
20
20
  ],
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pamela B2B Voice API SDK for JavaScript/TypeScript
2
+ * Pamela Enterprise Voice API SDK for JavaScript/TypeScript
3
3
  */
4
4
 
5
5
  import axios, { AxiosInstance, AxiosError } from 'axios';
@@ -64,9 +64,37 @@ export interface WebhookPayload {
64
64
  data: Record<string, any>;
65
65
  }
66
66
 
67
+ export class UsageClient {
68
+ private client: AxiosInstance;
69
+
70
+ constructor(client: AxiosInstance) {
71
+ this.client = client;
72
+ }
73
+
74
+ /**
75
+ * Get usage statistics for partner/project.
76
+ */
77
+ async get(period?: string): Promise<{
78
+ partner_id: string;
79
+ project_id?: string;
80
+ period: string;
81
+ call_count: number;
82
+ last_updated?: string;
83
+ quota?: {
84
+ partner_limit?: number;
85
+ project_limit?: number;
86
+ };
87
+ }> {
88
+ const params = period ? { period } : {};
89
+ const response = await this.client.get('/usage', { params });
90
+ return response.data;
91
+ }
92
+ }
93
+
67
94
  export class PamelaClient {
68
95
  private client: AxiosInstance;
69
96
  private apiKey: string;
97
+ public usage: UsageClient;
70
98
 
71
99
  constructor(config: PamelaClientConfig) {
72
100
  this.apiKey = config.apiKey;
@@ -81,6 +109,9 @@ export class PamelaClient {
81
109
  timeout: 30000,
82
110
  });
83
111
 
112
+ // Initialize usage client
113
+ this.usage = new UsageClient(this.client);
114
+
84
115
  // Add retry logic
85
116
  this.client.interceptors.response.use(
86
117
  (response) => response,
@@ -122,12 +153,18 @@ export class PamelaClient {
122
153
  */
123
154
  async listCalls(params?: {
124
155
  status?: string;
156
+ status_filter?: string;
125
157
  limit?: number;
126
158
  offset?: number;
127
159
  start_date?: string;
128
160
  end_date?: string;
129
161
  }): Promise<{ items: CallStatus[]; total: number; limit: number; offset: number }> {
130
- const response = await this.client.get('/calls', { params });
162
+ const normalizedParams = { ...params };
163
+ if (params?.status_filter || params?.status) {
164
+ normalizedParams.status_filter = params?.status_filter ?? params?.status;
165
+ delete (normalizedParams as { status?: string }).status;
166
+ }
167
+ const response = await this.client.get('/calls', { params: normalizedParams });
131
168
  // Backend returns { items: [...], total, limit, offset }
132
169
  return response.data;
133
170
  }
@@ -185,5 +222,7 @@ export class PamelaClient {
185
222
  }
186
223
  }
187
224
 
225
+ // Export as both default and named export for flexibility
188
226
  export default PamelaClient;
227
+ export { PamelaClient as Pamela };
189
228
 
@@ -1,136 +1,46 @@
1
1
  /**
2
2
  * Integration tests for Pamela JavaScript SDK.
3
- *
4
- * Tests against staging API or mocked responses.
3
+ *
4
+ * These tests run against staging only when env vars are provided.
5
5
  */
6
6
 
7
- import { Pamela } from "../src/index";
7
+ import PamelaClient from "../src/index";
8
8
 
9
- const TEST_API_URL = process.env.TEST_API_URL || "https://pamela-dev.up.railway.app";
10
- const TEST_API_KEY = process.env.TEST_API_KEY || "pk_test_placeholder";
9
+ const TEST_API_URL =
10
+ process.env.PAMELA_API_URL || "https://pamela-dev.up.railway.app";
11
+ const TEST_API_KEY = process.env.PAMELA_TEST_API_KEY;
12
+ const SHOULD_RUN = Boolean(TEST_API_KEY);
11
13
 
12
- describe("Pamela SDK", () => {
13
- let sdk: Pamela;
14
-
15
- beforeEach(() => {
16
- sdk = new Pamela({
17
- apiKey: TEST_API_KEY,
18
- apiUrl: TEST_API_URL,
19
- });
20
- });
21
-
22
- describe("Initialization", () => {
23
- it("should initialize with API key", () => {
24
- expect(sdk).toBeDefined();
25
- });
26
-
27
- it("should throw error without API key", () => {
28
- expect(() => {
29
- new Pamela({ apiKey: "" });
30
- }).toThrow();
31
- });
32
- });
33
-
34
- describe("Call Creation", () => {
35
- it("should create a call with required parameters", async () => {
36
- // Mock or use staging API
37
- const call = await sdk.calls.create({
38
- to: "+1234567890",
39
- from_: "+1987654321",
40
- country: "US",
41
- });
42
-
43
- expect(call).toBeDefined();
44
- expect(call.id).toBeDefined();
45
- expect(call.status).toBeDefined();
46
- });
47
-
48
- it("should handle invalid phone numbers", async () => {
49
- await expect(
50
- sdk.calls.create({
51
- to: "invalid",
52
- from_: "+1987654321",
53
- country: "US",
54
- })
55
- ).rejects.toThrow();
56
- });
57
- });
58
-
59
- describe("Call Status", () => {
60
- it("should get call status by ID", async () => {
61
- const callId = "test_call_id";
62
- const status = await sdk.calls.getStatus(callId);
63
-
64
- expect(status).toBeDefined();
65
- expect(status.id).toBe(callId);
66
- expect(status.status).toBeDefined();
67
- });
68
-
69
- it("should handle non-existent call ID", async () => {
70
- await expect(sdk.calls.getStatus("nonexistent")).rejects.toThrow();
14
+ describe("PamelaClient SDK", () => {
15
+ it("initializes with API key", () => {
16
+ const client = new PamelaClient({
17
+ apiKey: TEST_API_KEY || "pk_test_placeholder",
18
+ baseUrl: TEST_API_URL,
71
19
  });
20
+ expect(client).toBeDefined();
72
21
  });
22
+ });
73
23
 
74
- describe("Call Cancellation", () => {
75
- it("should cancel an in-progress call", async () => {
76
- const callId = "test_call_id";
77
- const result = await sdk.calls.cancel(callId);
78
-
79
- expect(result).toBeDefined();
80
- expect(result.success).toBe(true);
81
- });
24
+ (SHOULD_RUN ? describe : describe.skip)("PamelaClient integration", () => {
25
+ let client: PamelaClient;
82
26
 
83
- it("should handle cancelling already completed call", async () => {
84
- await expect(sdk.calls.cancel("completed_call")).rejects.toThrow();
27
+ beforeAll(() => {
28
+ client = new PamelaClient({
29
+ apiKey: TEST_API_KEY as string,
30
+ baseUrl: TEST_API_URL,
85
31
  });
86
32
  });
87
33
 
88
- describe("Usage", () => {
89
- it("should get usage statistics", async () => {
90
- const usage = await sdk.usage.get("2024-01");
91
-
92
- expect(usage).toBeDefined();
93
- expect(usage.call_count).toBeDefined();
94
- expect(usage.quota).toBeDefined();
95
- });
96
-
97
- it("should handle invalid period format", async () => {
98
- await expect(sdk.usage.get("invalid")).rejects.toThrow();
99
- });
34
+ it("lists calls", async () => {
35
+ const result = await client.listCalls({ limit: 1 });
36
+ expect(result).toBeDefined();
37
+ expect(Array.isArray(result.items)).toBe(true);
100
38
  });
101
39
 
102
- describe("Error Handling", () => {
103
- it("should handle network errors", async () => {
104
- // Test with invalid API URL
105
- const badSdk = new Pamela({
106
- apiKey: TEST_API_KEY,
107
- apiUrl: "https://invalid-url.example.com",
108
- });
109
-
110
- await expect(
111
- badSdk.calls.create({
112
- to: "+1234567890",
113
- from_: "+1987654321",
114
- country: "US",
115
- })
116
- ).rejects.toThrow();
117
- });
118
-
119
- it("should handle API errors (400, 401, 403, 500)", async () => {
120
- // Test with invalid API key
121
- const badSdk = new Pamela({
122
- apiKey: "invalid_key",
123
- apiUrl: TEST_API_URL,
124
- });
125
-
126
- await expect(
127
- badSdk.calls.create({
128
- to: "+1234567890",
129
- from_: "+1987654321",
130
- country: "US",
131
- })
132
- ).rejects.toThrow();
133
- });
40
+ it("gets usage", async () => {
41
+ const usage = await client.usage.get();
42
+ expect(usage).toBeDefined();
43
+ expect(usage.call_count).toBeDefined();
134
44
  });
135
45
  });
136
46