@sendly/node
Advanced tools
+757
-49
@@ -50,5 +50,10 @@ /** | ||
| * Message status values | ||
| * Note: "sending" was removed as it doesn't exist in the database | ||
| */ | ||
| type MessageStatus = "queued" | "sending" | "sent" | "delivered" | "failed"; | ||
| type MessageStatus = "queued" | "sent" | "delivered" | "failed"; | ||
| /** | ||
| * How the message was sent | ||
| */ | ||
| type SenderType = "number_pool" | "alphanumeric" | "sandbox"; | ||
| /** | ||
| * A sent or received SMS message | ||
@@ -78,2 +83,6 @@ */ | ||
| /** | ||
| * Message direction | ||
| */ | ||
| direction: "outbound" | "inbound"; | ||
| /** | ||
| * Error message if status is "failed" | ||
@@ -95,2 +104,21 @@ */ | ||
| /** | ||
| * How the message was sent | ||
| * - "number_pool": Sent from toll-free number pool (US/CA) | ||
| * - "alphanumeric": Sent with alphanumeric sender ID (international) | ||
| * - "sandbox": Sent in sandbox/test mode | ||
| */ | ||
| senderType?: SenderType; | ||
| /** | ||
| * Telnyx message ID for tracking | ||
| */ | ||
| telnyxMessageId?: string | null; | ||
| /** | ||
| * Warning message (e.g., when "from" is ignored for domestic messages) | ||
| */ | ||
| warning?: string; | ||
| /** | ||
| * Note about sender behavior (e.g., toll-free number pool explanation) | ||
| */ | ||
| senderNote?: string; | ||
| /** | ||
| * ISO 8601 timestamp when the message was created | ||
@@ -485,2 +513,215 @@ */ | ||
| /** | ||
| * Webhook event types | ||
| */ | ||
| type WebhookEventType = "message.sent" | "message.delivered" | "message.failed" | "message.bounced" | "message.queued"; | ||
| /** | ||
| * Circuit breaker state for webhook delivery | ||
| */ | ||
| type CircuitState = "closed" | "open" | "half_open"; | ||
| /** | ||
| * Webhook delivery status | ||
| */ | ||
| type DeliveryStatus = "pending" | "delivered" | "failed" | "cancelled"; | ||
| /** | ||
| * A configured webhook endpoint | ||
| */ | ||
| interface Webhook { | ||
| /** Unique webhook identifier (whk_xxx) */ | ||
| id: string; | ||
| /** HTTPS endpoint URL */ | ||
| url: string; | ||
| /** Event types this webhook subscribes to */ | ||
| events: WebhookEventType[]; | ||
| /** Optional description */ | ||
| description?: string; | ||
| /** Whether the webhook is active */ | ||
| isActive: boolean; | ||
| /** Number of consecutive failures */ | ||
| failureCount: number; | ||
| /** Last failure timestamp (ISO 8601) */ | ||
| lastFailureAt?: string | null; | ||
| /** Circuit breaker state */ | ||
| circuitState: CircuitState; | ||
| /** When circuit was opened (ISO 8601) */ | ||
| circuitOpenedAt?: string | null; | ||
| /** API version for payloads */ | ||
| apiVersion: string; | ||
| /** Custom metadata */ | ||
| metadata: Record<string, unknown>; | ||
| /** When webhook was created (ISO 8601) */ | ||
| createdAt: string; | ||
| /** When webhook was last updated (ISO 8601) */ | ||
| updatedAt: string; | ||
| /** Total delivery attempts */ | ||
| totalDeliveries: number; | ||
| /** Successful deliveries */ | ||
| successfulDeliveries: number; | ||
| /** Success rate (0-100) */ | ||
| successRate: number; | ||
| /** Last successful delivery (ISO 8601) */ | ||
| lastDeliveryAt?: string | null; | ||
| } | ||
| /** | ||
| * Response when creating a webhook (includes secret once) | ||
| */ | ||
| interface WebhookCreatedResponse extends Webhook { | ||
| /** Webhook signing secret - only shown once at creation */ | ||
| secret: string; | ||
| } | ||
| /** | ||
| * Options for creating a webhook | ||
| */ | ||
| interface CreateWebhookOptions { | ||
| /** HTTPS endpoint URL */ | ||
| url: string; | ||
| /** Event types to subscribe to */ | ||
| events: WebhookEventType[]; | ||
| /** Optional description */ | ||
| description?: string; | ||
| /** Custom metadata */ | ||
| metadata?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * Options for updating a webhook | ||
| */ | ||
| interface UpdateWebhookOptions { | ||
| /** New URL */ | ||
| url?: string; | ||
| /** New event subscriptions */ | ||
| events?: WebhookEventType[]; | ||
| /** New description */ | ||
| description?: string; | ||
| /** Enable/disable webhook */ | ||
| isActive?: boolean; | ||
| /** Custom metadata */ | ||
| metadata?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * A webhook delivery attempt | ||
| */ | ||
| interface WebhookDelivery { | ||
| /** Unique delivery identifier (del_xxx) */ | ||
| id: string; | ||
| /** Webhook ID this delivery belongs to */ | ||
| webhookId: string; | ||
| /** Event ID for idempotency */ | ||
| eventId: string; | ||
| /** Event type */ | ||
| eventType: WebhookEventType; | ||
| /** Attempt number (1-6) */ | ||
| attemptNumber: number; | ||
| /** Maximum attempts allowed */ | ||
| maxAttempts: number; | ||
| /** Delivery status */ | ||
| status: DeliveryStatus; | ||
| /** HTTP response status code */ | ||
| responseStatusCode?: number; | ||
| /** Response time in milliseconds */ | ||
| responseTimeMs?: number; | ||
| /** Error message if failed */ | ||
| errorMessage?: string; | ||
| /** Error code if failed */ | ||
| errorCode?: string; | ||
| /** Next retry time (ISO 8601) */ | ||
| nextRetryAt?: string; | ||
| /** When delivery was created (ISO 8601) */ | ||
| createdAt: string; | ||
| /** When delivery succeeded (ISO 8601) */ | ||
| deliveredAt?: string; | ||
| } | ||
| /** | ||
| * Response from testing a webhook | ||
| */ | ||
| interface WebhookTestResult { | ||
| /** Whether test was successful */ | ||
| success: boolean; | ||
| /** HTTP status code from endpoint */ | ||
| statusCode?: number; | ||
| /** Response time in milliseconds */ | ||
| responseTimeMs?: number; | ||
| /** Error message if failed */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * Response from rotating webhook secret | ||
| */ | ||
| interface WebhookSecretRotation { | ||
| /** The webhook */ | ||
| webhook: Webhook; | ||
| /** New signing secret */ | ||
| newSecret: string; | ||
| /** When old secret expires (ISO 8601) */ | ||
| oldSecretExpiresAt: string; | ||
| /** Message about grace period */ | ||
| message: string; | ||
| } | ||
| /** | ||
| * Account information | ||
| */ | ||
| interface Account { | ||
| /** User ID */ | ||
| id: string; | ||
| /** Email address */ | ||
| email: string; | ||
| /** Display name */ | ||
| name?: string; | ||
| /** Account creation date (ISO 8601) */ | ||
| createdAt: string; | ||
| } | ||
| /** | ||
| * Credit balance information | ||
| */ | ||
| interface Credits { | ||
| /** Available credit balance */ | ||
| balance: number; | ||
| /** Credits reserved for scheduled messages */ | ||
| reservedBalance: number; | ||
| /** Total usable credits (balance - reserved) */ | ||
| availableBalance: number; | ||
| } | ||
| /** | ||
| * A credit transaction record | ||
| */ | ||
| interface CreditTransaction { | ||
| /** Transaction ID */ | ||
| id: string; | ||
| /** Transaction type */ | ||
| type: "purchase" | "usage" | "refund" | "adjustment" | "bonus"; | ||
| /** Amount (positive for credits in, negative for credits out) */ | ||
| amount: number; | ||
| /** Balance after transaction */ | ||
| balanceAfter: number; | ||
| /** Transaction description */ | ||
| description: string; | ||
| /** Related message ID (for usage transactions) */ | ||
| messageId?: string; | ||
| /** When transaction occurred (ISO 8601) */ | ||
| createdAt: string; | ||
| } | ||
| /** | ||
| * An API key | ||
| */ | ||
| interface ApiKey { | ||
| /** Key ID */ | ||
| id: string; | ||
| /** Key name/label */ | ||
| name: string; | ||
| /** Key type */ | ||
| type: "test" | "live"; | ||
| /** Key prefix (for identification) */ | ||
| prefix: string; | ||
| /** Last 4 characters of key */ | ||
| lastFour: string; | ||
| /** Permissions granted */ | ||
| permissions: string[]; | ||
| /** When key was created (ISO 8601) */ | ||
| createdAt: string; | ||
| /** When key was last used (ISO 8601) */ | ||
| lastUsedAt?: string | null; | ||
| /** When key expires (ISO 8601) */ | ||
| expiresAt?: string | null; | ||
| /** Whether key is revoked */ | ||
| isRevoked: boolean; | ||
| } | ||
| /** | ||
| * Test phone numbers for sandbox mode | ||
@@ -813,2 +1054,327 @@ */ | ||
| /** | ||
| * Webhooks Resource | ||
| * @packageDocumentation | ||
| */ | ||
| /** | ||
| * Webhooks API resource | ||
| * | ||
| * Manage webhook endpoints for receiving real-time message status updates. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Create a webhook | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks/sendly', | ||
| * events: ['message.delivered', 'message.failed'] | ||
| * }); | ||
| * | ||
| * // IMPORTANT: Save the secret - it's only shown once! | ||
| * console.log('Secret:', webhook.secret); | ||
| * | ||
| * // List webhooks | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * // Test a webhook | ||
| * const result = await sendly.webhooks.test(webhook.id); | ||
| * ``` | ||
| */ | ||
| declare class WebhooksResource { | ||
| private readonly http; | ||
| constructor(http: HttpClient); | ||
| /** | ||
| * Create a new webhook endpoint | ||
| * | ||
| * @param options - Webhook configuration | ||
| * @returns The created webhook with signing secret (shown only once!) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks/sendly', | ||
| * events: ['message.delivered', 'message.failed'], | ||
| * description: 'Production webhook' | ||
| * }); | ||
| * | ||
| * // IMPORTANT: Save this secret securely - it's only shown once! | ||
| * console.log('Webhook secret:', webhook.secret); | ||
| * ``` | ||
| * | ||
| * @throws {ValidationError} If the URL is invalid or events are empty | ||
| * @throws {AuthenticationError} If the API key is invalid | ||
| */ | ||
| create(options: CreateWebhookOptions): Promise<WebhookCreatedResponse>; | ||
| /** | ||
| * List all webhooks | ||
| * | ||
| * @returns Array of webhook configurations | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * for (const webhook of webhooks) { | ||
| * console.log(`${webhook.id}: ${webhook.url} (${webhook.isActive ? 'active' : 'inactive'})`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| list(): Promise<Webhook[]>; | ||
| /** | ||
| * Get a specific webhook by ID | ||
| * | ||
| * @param id - Webhook ID (whk_xxx) | ||
| * @returns The webhook details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhook = await sendly.webhooks.get('whk_xxx'); | ||
| * console.log(`Success rate: ${webhook.successRate}%`); | ||
| * ``` | ||
| * | ||
| * @throws {NotFoundError} If the webhook doesn't exist | ||
| */ | ||
| get(id: string): Promise<Webhook>; | ||
| /** | ||
| * Update a webhook configuration | ||
| * | ||
| * @param id - Webhook ID | ||
| * @param options - Fields to update | ||
| * @returns The updated webhook | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Update URL | ||
| * await sendly.webhooks.update('whk_xxx', { | ||
| * url: 'https://new-endpoint.example.com/webhooks' | ||
| * }); | ||
| * | ||
| * // Disable webhook | ||
| * await sendly.webhooks.update('whk_xxx', { isActive: false }); | ||
| * | ||
| * // Change event subscriptions | ||
| * await sendly.webhooks.update('whk_xxx', { | ||
| * events: ['message.delivered'] | ||
| * }); | ||
| * ``` | ||
| */ | ||
| update(id: string, options: UpdateWebhookOptions): Promise<Webhook>; | ||
| /** | ||
| * Delete a webhook | ||
| * | ||
| * @param id - Webhook ID | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await sendly.webhooks.delete('whk_xxx'); | ||
| * ``` | ||
| * | ||
| * @throws {NotFoundError} If the webhook doesn't exist | ||
| */ | ||
| delete(id: string): Promise<void>; | ||
| /** | ||
| * Send a test event to a webhook endpoint | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns Test result with response details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const result = await sendly.webhooks.test('whk_xxx'); | ||
| * | ||
| * if (result.success) { | ||
| * console.log(`Test passed! Response time: ${result.responseTimeMs}ms`); | ||
| * } else { | ||
| * console.log(`Test failed: ${result.error}`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| test(id: string): Promise<WebhookTestResult>; | ||
| /** | ||
| * Rotate the webhook signing secret | ||
| * | ||
| * The old secret remains valid for 24 hours to allow for graceful migration. | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns New secret and expiration info | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const rotation = await sendly.webhooks.rotateSecret('whk_xxx'); | ||
| * | ||
| * // Update your webhook handler with the new secret | ||
| * console.log('New secret:', rotation.newSecret); | ||
| * console.log('Old secret expires:', rotation.oldSecretExpiresAt); | ||
| * ``` | ||
| */ | ||
| rotateSecret(id: string): Promise<WebhookSecretRotation>; | ||
| /** | ||
| * Get delivery history for a webhook | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns Array of delivery attempts | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const deliveries = await sendly.webhooks.getDeliveries('whk_xxx'); | ||
| * | ||
| * for (const delivery of deliveries) { | ||
| * console.log(`${delivery.eventType}: ${delivery.status} (${delivery.responseTimeMs}ms)`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| getDeliveries(id: string): Promise<WebhookDelivery[]>; | ||
| /** | ||
| * Retry a failed delivery | ||
| * | ||
| * @param webhookId - Webhook ID | ||
| * @param deliveryId - Delivery ID | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await sendly.webhooks.retryDelivery('whk_xxx', 'del_yyy'); | ||
| * ``` | ||
| */ | ||
| retryDelivery(webhookId: string, deliveryId: string): Promise<void>; | ||
| /** | ||
| * List available event types | ||
| * | ||
| * @returns Array of event type strings | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const eventTypes = await sendly.webhooks.listEventTypes(); | ||
| * console.log('Available events:', eventTypes); | ||
| * // ['message.sent', 'message.delivered', 'message.failed', 'message.bounced'] | ||
| * ``` | ||
| */ | ||
| listEventTypes(): Promise<WebhookEventType[]>; | ||
| } | ||
| /** | ||
| * Account Resource | ||
| * @packageDocumentation | ||
| */ | ||
| /** | ||
| * Account API resource | ||
| * | ||
| * Access account information, credit balance, and API keys. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Get credit balance | ||
| * const credits = await sendly.account.getCredits(); | ||
| * console.log(`Available: ${credits.availableBalance} credits`); | ||
| * | ||
| * // Get transaction history | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * // List API keys | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * ``` | ||
| */ | ||
| declare class AccountResource { | ||
| private readonly http; | ||
| constructor(http: HttpClient); | ||
| /** | ||
| * Get account information | ||
| * | ||
| * @returns Account details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const account = await sendly.account.get(); | ||
| * console.log(`Account: ${account.email}`); | ||
| * ``` | ||
| */ | ||
| get(): Promise<Account>; | ||
| /** | ||
| * Get credit balance | ||
| * | ||
| * @returns Current credit balance and reserved credits | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const credits = await sendly.account.getCredits(); | ||
| * | ||
| * console.log(`Total balance: ${credits.balance}`); | ||
| * console.log(`Reserved (scheduled): ${credits.reservedBalance}`); | ||
| * console.log(`Available to use: ${credits.availableBalance}`); | ||
| * ``` | ||
| */ | ||
| getCredits(): Promise<Credits>; | ||
| /** | ||
| * Get credit transaction history | ||
| * | ||
| * @param options - Pagination options | ||
| * @returns Array of credit transactions | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * for (const tx of transactions) { | ||
| * const sign = tx.amount > 0 ? '+' : ''; | ||
| * console.log(`${tx.type}: ${sign}${tx.amount} credits - ${tx.description}`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| getCreditTransactions(options?: { | ||
| limit?: number; | ||
| offset?: number; | ||
| }): Promise<CreditTransaction[]>; | ||
| /** | ||
| * List API keys for the account | ||
| * | ||
| * Note: This returns key metadata, not the actual secret keys. | ||
| * | ||
| * @returns Array of API keys | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * | ||
| * for (const key of keys) { | ||
| * console.log(`${key.name}: ${key.prefix}...${key.lastFour} (${key.type})`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| listApiKeys(): Promise<ApiKey[]>; | ||
| /** | ||
| * Get a specific API key by ID | ||
| * | ||
| * @param id - API key ID | ||
| * @returns API key details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const key = await sendly.account.getApiKey('key_xxx'); | ||
| * console.log(`Last used: ${key.lastUsedAt}`); | ||
| * ``` | ||
| */ | ||
| getApiKey(id: string): Promise<ApiKey>; | ||
| /** | ||
| * Get usage statistics for an API key | ||
| * | ||
| * @param id - API key ID | ||
| * @returns Usage statistics | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const usage = await sendly.account.getApiKeyUsage('key_xxx'); | ||
| * console.log(`Messages sent: ${usage.messagesSent}`); | ||
| * ``` | ||
| */ | ||
| getApiKeyUsage(id: string): Promise<{ | ||
| keyId: string; | ||
| messagesSent: number; | ||
| messagesDelivered: number; | ||
| messagesFailed: number; | ||
| creditsUsed: number; | ||
| periodStart: string; | ||
| periodEnd: string; | ||
| }>; | ||
| } | ||
| /** | ||
| * Sendly Client | ||
@@ -866,2 +1432,37 @@ * @packageDocumentation | ||
| readonly messages: MessagesResource; | ||
| /** | ||
| * Webhooks API resource | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Create a webhook | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks', | ||
| * events: ['message.delivered', 'message.failed'] | ||
| * }); | ||
| * | ||
| * // List webhooks | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * // Test a webhook | ||
| * await sendly.webhooks.test('whk_xxx'); | ||
| * ``` | ||
| */ | ||
| readonly webhooks: WebhooksResource; | ||
| /** | ||
| * Account API resource | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Get credit balance | ||
| * const credits = await sendly.account.getCredits(); | ||
| * | ||
| * // Get transaction history | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * // List API keys | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * ``` | ||
| */ | ||
| readonly account: AccountResource; | ||
| private readonly http; | ||
@@ -1029,49 +1630,82 @@ private readonly config; | ||
| */ | ||
| /** | ||
| * Webhook event types | ||
| */ | ||
| type WebhookEventType = "message.queued" | "message.sent" | "message.delivered" | "message.failed" | "message.undelivered"; | ||
| /** | ||
| * Message status in webhook events | ||
| */ | ||
| type WebhookMessageStatus = "queued" | "sent" | "delivered" | "failed" | "undelivered"; | ||
| type WebhookMessageStatus = "queued" | "sent" | "delivered" | "failed"; | ||
| /** | ||
| * Webhook message data payload | ||
| * Message object within webhook payload | ||
| * Matches the structure sent by Sendly servers | ||
| */ | ||
| interface WebhookMessageData { | ||
| /** Unique message identifier */ | ||
| messageId: string; | ||
| interface WebhookMessageObject { | ||
| /** Message ID (msg_xxx) */ | ||
| id: string; | ||
| /** Recipient phone number (E.164 format) */ | ||
| to: string; | ||
| /** Sender phone number or ID */ | ||
| from: string; | ||
| /** Message text content */ | ||
| text: string; | ||
| /** Current message status */ | ||
| status: WebhookMessageStatus; | ||
| /** Destination phone number */ | ||
| to: string; | ||
| /** Sender ID or phone number */ | ||
| from: string; | ||
| /** Error message if failed */ | ||
| error?: string; | ||
| /** Error code if failed */ | ||
| errorCode?: string; | ||
| /** ISO 8601 timestamp when delivered */ | ||
| deliveredAt?: string; | ||
| /** ISO 8601 timestamp when failed */ | ||
| failedAt?: string; | ||
| /** Message direction */ | ||
| direction: "outbound" | "inbound"; | ||
| /** Number of SMS segments */ | ||
| segments: number; | ||
| /** Credits charged */ | ||
| creditsUsed: number; | ||
| /** Credits charged for this message */ | ||
| credits_used: number; | ||
| /** Unix timestamp when message was created */ | ||
| created_at: number; | ||
| /** Unix timestamp when message was delivered (if applicable) */ | ||
| delivered_at?: number; | ||
| /** Error message if status is 'failed' */ | ||
| error?: string; | ||
| /** Custom metadata attached to the message */ | ||
| metadata?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * Webhook event structure | ||
| * Webhook event payload from Sendly | ||
| * This is the exact structure sent to your webhook endpoints | ||
| * | ||
| * @example | ||
| * ```json | ||
| * { | ||
| * "id": "evt_abc123", | ||
| * "type": "message.delivered", | ||
| * "api_version": "2024-01", | ||
| * "created": 1702000000, | ||
| * "livemode": true, | ||
| * "data": { | ||
| * "object": { | ||
| * "id": "msg_xyz789", | ||
| * "to": "+15551234567", | ||
| * "from": "+15559876543", | ||
| * "text": "Hello!", | ||
| * "status": "delivered", | ||
| * "direction": "outbound", | ||
| * "segments": 1, | ||
| * "credits_used": 1, | ||
| * "created_at": 1702000000, | ||
| * "delivered_at": 1702000005 | ||
| * } | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| interface WebhookEvent { | ||
| /** Unique event identifier */ | ||
| /** Unique event identifier (evt_xxx) for idempotency */ | ||
| id: string; | ||
| /** Event type */ | ||
| type: WebhookEventType; | ||
| /** Event data payload */ | ||
| data: WebhookMessageData; | ||
| /** ISO 8601 timestamp when event was created */ | ||
| createdAt: string; | ||
| /** API version that generated this event */ | ||
| apiVersion: string; | ||
| api_version: string; | ||
| /** Unix timestamp (seconds) when event was created */ | ||
| created: number; | ||
| /** Whether this event is from live mode (true) or sandbox (false) */ | ||
| livemode: boolean; | ||
| /** Event data containing the message object */ | ||
| data: { | ||
| /** The message object that triggered this event */ | ||
| object: WebhookMessageObject; | ||
| }; | ||
| } | ||
@@ -1087,5 +1721,12 @@ /** | ||
| * | ||
| * @param payload - Raw request body as string | ||
| * Sendly signs webhooks using HMAC-SHA256. The signature format is: | ||
| * `sha256=<hex_digest>` | ||
| * | ||
| * The signed payload is: `<timestamp>.<json_body>` | ||
| * | ||
| * @param payload - Raw request body as string (JSON) | ||
| * @param signature - X-Sendly-Signature header value | ||
| * @param secret - Your webhook secret from dashboard | ||
| * @param timestamp - X-Sendly-Timestamp header value (optional, for enhanced verification) | ||
| * @param toleranceSeconds - Maximum age of webhook in seconds (default: 300 = 5 minutes) | ||
| * @returns true if signature is valid | ||
@@ -1097,2 +1738,3 @@ * | ||
| * | ||
| * // Basic verification | ||
| * const isValid = verifyWebhookSignature( | ||
@@ -1104,2 +1746,10 @@ * req.body, // raw body string | ||
| * | ||
| * // With timestamp verification (recommended) | ||
| * const isValid = verifyWebhookSignature( | ||
| * req.body, | ||
| * req.headers['x-sendly-signature'], | ||
| * process.env.WEBHOOK_SECRET, | ||
| * req.headers['x-sendly-timestamp'] | ||
| * ); | ||
| * | ||
| * if (!isValid) { | ||
@@ -1110,3 +1760,3 @@ * return res.status(401).send('Invalid signature'); | ||
| */ | ||
| declare function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean; | ||
| declare function verifyWebhookSignature(payload: string, signature: string, secret: string, timestamp?: string, toleranceSeconds?: number): boolean; | ||
| /** | ||
@@ -1118,2 +1768,3 @@ * Parse and verify a webhook event | ||
| * @param secret - Your webhook secret from dashboard | ||
| * @param timestamp - X-Sendly-Timestamp header value (optional) | ||
| * @returns Parsed webhook event | ||
@@ -1130,3 +1781,4 @@ * @throws {WebhookSignatureError} If signature is invalid | ||
| * req.headers['x-sendly-signature'], | ||
| * process.env.WEBHOOK_SECRET | ||
| * process.env.WEBHOOK_SECRET, | ||
| * req.headers['x-sendly-timestamp'] | ||
| * ); | ||
@@ -1136,6 +1788,6 @@ * | ||
| * case 'message.delivered': | ||
| * console.log(`Message ${event.data.messageId} delivered!`); | ||
| * console.log(`Message ${event.data.object.id} delivered!`); | ||
| * break; | ||
| * case 'message.failed': | ||
| * console.log(`Message failed: ${event.data.error}`); | ||
| * console.log(`Message failed: ${event.data.object.error}`); | ||
| * break; | ||
@@ -1151,3 +1803,3 @@ * } | ||
| */ | ||
| declare function parseWebhookEvent(payload: string, signature: string, secret: string): WebhookEvent; | ||
| declare function parseWebhookEvent(payload: string, signature: string, secret: string, timestamp?: string): WebhookEvent; | ||
| /** | ||
@@ -1165,11 +1817,27 @@ * Generate a webhook signature for testing purposes | ||
| * // For testing your webhook handler | ||
| * const timestamp = Math.floor(Date.now() / 1000); | ||
| * const testPayload = JSON.stringify({ | ||
| * id: 'evt_test', | ||
| * type: 'message.delivered', | ||
| * data: { messageId: 'msg_123', status: 'delivered' }, | ||
| * createdAt: new Date().toISOString(), | ||
| * apiVersion: '2025-01-01' | ||
| * api_version: '2024-01', | ||
| * created: timestamp, | ||
| * livemode: false, | ||
| * data: { | ||
| * object: { | ||
| * id: 'msg_123', | ||
| * to: '+15551234567', | ||
| * from: '+15559876543', | ||
| * text: 'Test message', | ||
| * status: 'delivered', | ||
| * direction: 'outbound', | ||
| * segments: 1, | ||
| * credits_used: 1, | ||
| * created_at: timestamp, | ||
| * delivered_at: timestamp | ||
| * } | ||
| * } | ||
| * }); | ||
| * | ||
| * const signature = generateWebhookSignature(testPayload, 'test_secret'); | ||
| * const signedPayload = `${timestamp}.${testPayload}`; | ||
| * const signature = generateWebhookSignature(signedPayload, 'test_secret'); | ||
| * ``` | ||
@@ -1187,7 +1855,21 @@ */ | ||
| * | ||
| * // Verify signature | ||
| * const isValid = webhooks.verify(payload, signature); | ||
| * // In your Express/Fastify/etc handler: | ||
| * app.post('/webhooks/sendly', (req, res) => { | ||
| * try { | ||
| * const event = webhooks.parse( | ||
| * req.body, | ||
| * req.headers['x-sendly-signature'], | ||
| * req.headers['x-sendly-timestamp'] | ||
| * ); | ||
| * | ||
| * // Parse event | ||
| * const event = webhooks.parse(payload, signature); | ||
| * // Handle the event | ||
| * if (event.type === 'message.delivered') { | ||
| * console.log(`Message ${event.data.object.id} delivered!`); | ||
| * } | ||
| * | ||
| * res.status(200).send('OK'); | ||
| * } catch (err) { | ||
| * res.status(401).send('Invalid signature'); | ||
| * } | ||
| * }); | ||
| * ``` | ||
@@ -1206,4 +1888,5 @@ */ | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param timestamp - X-Sendly-Timestamp header (optional) | ||
| */ | ||
| verify(payload: string, signature: string): boolean; | ||
| verify(payload: string, signature: string, timestamp?: string): boolean; | ||
| /** | ||
@@ -1213,11 +1896,36 @@ * Parse and verify a webhook event | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param timestamp - X-Sendly-Timestamp header (optional) | ||
| */ | ||
| parse(payload: string, signature: string): WebhookEvent; | ||
| parse(payload: string, signature: string, timestamp?: string): WebhookEvent; | ||
| /** | ||
| * Generate a signature for testing | ||
| * @param payload - Payload to sign (should include timestamp prefix if using timestamps) | ||
| */ | ||
| sign(payload: string): string; | ||
| /** | ||
| * Verify a webhook signature (static method for backwards compatibility) | ||
| * @param payload - Raw request body | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param secret - Your webhook secret | ||
| */ | ||
| static verifySignature(payload: string, signature: string, secret: string): boolean; | ||
| /** | ||
| * Parse and verify a webhook event (static method for backwards compatibility) | ||
| * @param payload - Raw request body | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param secret - Your webhook secret | ||
| */ | ||
| static parseEvent(payload: string, signature: string, secret: string): WebhookEvent; | ||
| /** | ||
| * Generate a webhook signature (static method for backwards compatibility) | ||
| * @param payload - Payload to sign | ||
| * @param secret - Secret to use for signing | ||
| */ | ||
| sign(payload: string): string; | ||
| static generateSignature(payload: string, secret: string): string; | ||
| } | ||
| /** | ||
| * @deprecated Use WebhookMessageObject instead | ||
| */ | ||
| type WebhookMessageData = WebhookMessageObject; | ||
| export { ALL_SUPPORTED_COUNTRIES, type ApiErrorResponse, AuthenticationError, CREDITS_PER_SMS, InsufficientCreditsError, type ListMessagesOptions, type Message, type MessageListResponse, type MessageStatus, NetworkError, NotFoundError, type PricingTier, RateLimitError, type RateLimitInfo, SANDBOX_TEST_NUMBERS, SUPPORTED_COUNTRIES, type SendMessageRequest, Sendly, type SendlyConfig, SendlyError, type SendlyErrorCode, TimeoutError, ValidationError, type WebhookEvent, type WebhookEventType, type WebhookMessageData, type WebhookMessageStatus, WebhookSignatureError, Webhooks, calculateSegments, Sendly as default, generateWebhookSignature, getCountryFromPhone, isCountrySupported, parseWebhookEvent, validateMessageText, validatePhoneNumber, validateSenderId, verifyWebhookSignature }; | ||
| export { ALL_SUPPORTED_COUNTRIES, type Account, type ApiErrorResponse, type ApiKey, AuthenticationError, type BatchListResponse, type BatchMessageItem, type BatchMessageRequest, type BatchMessageResponse, type BatchMessageResult, type BatchStatus, CREDITS_PER_SMS, type CancelledMessageResponse, type CircuitState, type CreateWebhookOptions, type CreditTransaction, type Credits, type DeliveryStatus, InsufficientCreditsError, type ListBatchesOptions, type ListMessagesOptions, type ListScheduledMessagesOptions, type Message, type MessageListResponse, type MessageStatus, NetworkError, NotFoundError, type PricingTier, RateLimitError, type RateLimitInfo, SANDBOX_TEST_NUMBERS, SUPPORTED_COUNTRIES, type ScheduleMessageRequest, type ScheduledMessage, type ScheduledMessageListResponse, type ScheduledMessageStatus, type SendMessageRequest, type SenderType, Sendly, type SendlyConfig, SendlyError, type SendlyErrorCode, TimeoutError, type UpdateWebhookOptions, ValidationError, type Webhook, type WebhookCreatedResponse, type WebhookDelivery, type WebhookEvent, type WebhookEventType, type WebhookMessageData, type WebhookMessageStatus, type WebhookSecretRotation, WebhookSignatureError, type WebhookTestResult, Webhooks, calculateSegments, Sendly as default, generateWebhookSignature, getCountryFromPhone, isCountrySupported, parseWebhookEvent, validateMessageText, validatePhoneNumber, validateSenderId, verifyWebhookSignature }; |
+757
-49
@@ -50,5 +50,10 @@ /** | ||
| * Message status values | ||
| * Note: "sending" was removed as it doesn't exist in the database | ||
| */ | ||
| type MessageStatus = "queued" | "sending" | "sent" | "delivered" | "failed"; | ||
| type MessageStatus = "queued" | "sent" | "delivered" | "failed"; | ||
| /** | ||
| * How the message was sent | ||
| */ | ||
| type SenderType = "number_pool" | "alphanumeric" | "sandbox"; | ||
| /** | ||
| * A sent or received SMS message | ||
@@ -78,2 +83,6 @@ */ | ||
| /** | ||
| * Message direction | ||
| */ | ||
| direction: "outbound" | "inbound"; | ||
| /** | ||
| * Error message if status is "failed" | ||
@@ -95,2 +104,21 @@ */ | ||
| /** | ||
| * How the message was sent | ||
| * - "number_pool": Sent from toll-free number pool (US/CA) | ||
| * - "alphanumeric": Sent with alphanumeric sender ID (international) | ||
| * - "sandbox": Sent in sandbox/test mode | ||
| */ | ||
| senderType?: SenderType; | ||
| /** | ||
| * Telnyx message ID for tracking | ||
| */ | ||
| telnyxMessageId?: string | null; | ||
| /** | ||
| * Warning message (e.g., when "from" is ignored for domestic messages) | ||
| */ | ||
| warning?: string; | ||
| /** | ||
| * Note about sender behavior (e.g., toll-free number pool explanation) | ||
| */ | ||
| senderNote?: string; | ||
| /** | ||
| * ISO 8601 timestamp when the message was created | ||
@@ -485,2 +513,215 @@ */ | ||
| /** | ||
| * Webhook event types | ||
| */ | ||
| type WebhookEventType = "message.sent" | "message.delivered" | "message.failed" | "message.bounced" | "message.queued"; | ||
| /** | ||
| * Circuit breaker state for webhook delivery | ||
| */ | ||
| type CircuitState = "closed" | "open" | "half_open"; | ||
| /** | ||
| * Webhook delivery status | ||
| */ | ||
| type DeliveryStatus = "pending" | "delivered" | "failed" | "cancelled"; | ||
| /** | ||
| * A configured webhook endpoint | ||
| */ | ||
| interface Webhook { | ||
| /** Unique webhook identifier (whk_xxx) */ | ||
| id: string; | ||
| /** HTTPS endpoint URL */ | ||
| url: string; | ||
| /** Event types this webhook subscribes to */ | ||
| events: WebhookEventType[]; | ||
| /** Optional description */ | ||
| description?: string; | ||
| /** Whether the webhook is active */ | ||
| isActive: boolean; | ||
| /** Number of consecutive failures */ | ||
| failureCount: number; | ||
| /** Last failure timestamp (ISO 8601) */ | ||
| lastFailureAt?: string | null; | ||
| /** Circuit breaker state */ | ||
| circuitState: CircuitState; | ||
| /** When circuit was opened (ISO 8601) */ | ||
| circuitOpenedAt?: string | null; | ||
| /** API version for payloads */ | ||
| apiVersion: string; | ||
| /** Custom metadata */ | ||
| metadata: Record<string, unknown>; | ||
| /** When webhook was created (ISO 8601) */ | ||
| createdAt: string; | ||
| /** When webhook was last updated (ISO 8601) */ | ||
| updatedAt: string; | ||
| /** Total delivery attempts */ | ||
| totalDeliveries: number; | ||
| /** Successful deliveries */ | ||
| successfulDeliveries: number; | ||
| /** Success rate (0-100) */ | ||
| successRate: number; | ||
| /** Last successful delivery (ISO 8601) */ | ||
| lastDeliveryAt?: string | null; | ||
| } | ||
| /** | ||
| * Response when creating a webhook (includes secret once) | ||
| */ | ||
| interface WebhookCreatedResponse extends Webhook { | ||
| /** Webhook signing secret - only shown once at creation */ | ||
| secret: string; | ||
| } | ||
| /** | ||
| * Options for creating a webhook | ||
| */ | ||
| interface CreateWebhookOptions { | ||
| /** HTTPS endpoint URL */ | ||
| url: string; | ||
| /** Event types to subscribe to */ | ||
| events: WebhookEventType[]; | ||
| /** Optional description */ | ||
| description?: string; | ||
| /** Custom metadata */ | ||
| metadata?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * Options for updating a webhook | ||
| */ | ||
| interface UpdateWebhookOptions { | ||
| /** New URL */ | ||
| url?: string; | ||
| /** New event subscriptions */ | ||
| events?: WebhookEventType[]; | ||
| /** New description */ | ||
| description?: string; | ||
| /** Enable/disable webhook */ | ||
| isActive?: boolean; | ||
| /** Custom metadata */ | ||
| metadata?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * A webhook delivery attempt | ||
| */ | ||
| interface WebhookDelivery { | ||
| /** Unique delivery identifier (del_xxx) */ | ||
| id: string; | ||
| /** Webhook ID this delivery belongs to */ | ||
| webhookId: string; | ||
| /** Event ID for idempotency */ | ||
| eventId: string; | ||
| /** Event type */ | ||
| eventType: WebhookEventType; | ||
| /** Attempt number (1-6) */ | ||
| attemptNumber: number; | ||
| /** Maximum attempts allowed */ | ||
| maxAttempts: number; | ||
| /** Delivery status */ | ||
| status: DeliveryStatus; | ||
| /** HTTP response status code */ | ||
| responseStatusCode?: number; | ||
| /** Response time in milliseconds */ | ||
| responseTimeMs?: number; | ||
| /** Error message if failed */ | ||
| errorMessage?: string; | ||
| /** Error code if failed */ | ||
| errorCode?: string; | ||
| /** Next retry time (ISO 8601) */ | ||
| nextRetryAt?: string; | ||
| /** When delivery was created (ISO 8601) */ | ||
| createdAt: string; | ||
| /** When delivery succeeded (ISO 8601) */ | ||
| deliveredAt?: string; | ||
| } | ||
| /** | ||
| * Response from testing a webhook | ||
| */ | ||
| interface WebhookTestResult { | ||
| /** Whether test was successful */ | ||
| success: boolean; | ||
| /** HTTP status code from endpoint */ | ||
| statusCode?: number; | ||
| /** Response time in milliseconds */ | ||
| responseTimeMs?: number; | ||
| /** Error message if failed */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * Response from rotating webhook secret | ||
| */ | ||
| interface WebhookSecretRotation { | ||
| /** The webhook */ | ||
| webhook: Webhook; | ||
| /** New signing secret */ | ||
| newSecret: string; | ||
| /** When old secret expires (ISO 8601) */ | ||
| oldSecretExpiresAt: string; | ||
| /** Message about grace period */ | ||
| message: string; | ||
| } | ||
| /** | ||
| * Account information | ||
| */ | ||
| interface Account { | ||
| /** User ID */ | ||
| id: string; | ||
| /** Email address */ | ||
| email: string; | ||
| /** Display name */ | ||
| name?: string; | ||
| /** Account creation date (ISO 8601) */ | ||
| createdAt: string; | ||
| } | ||
| /** | ||
| * Credit balance information | ||
| */ | ||
| interface Credits { | ||
| /** Available credit balance */ | ||
| balance: number; | ||
| /** Credits reserved for scheduled messages */ | ||
| reservedBalance: number; | ||
| /** Total usable credits (balance - reserved) */ | ||
| availableBalance: number; | ||
| } | ||
| /** | ||
| * A credit transaction record | ||
| */ | ||
| interface CreditTransaction { | ||
| /** Transaction ID */ | ||
| id: string; | ||
| /** Transaction type */ | ||
| type: "purchase" | "usage" | "refund" | "adjustment" | "bonus"; | ||
| /** Amount (positive for credits in, negative for credits out) */ | ||
| amount: number; | ||
| /** Balance after transaction */ | ||
| balanceAfter: number; | ||
| /** Transaction description */ | ||
| description: string; | ||
| /** Related message ID (for usage transactions) */ | ||
| messageId?: string; | ||
| /** When transaction occurred (ISO 8601) */ | ||
| createdAt: string; | ||
| } | ||
| /** | ||
| * An API key | ||
| */ | ||
| interface ApiKey { | ||
| /** Key ID */ | ||
| id: string; | ||
| /** Key name/label */ | ||
| name: string; | ||
| /** Key type */ | ||
| type: "test" | "live"; | ||
| /** Key prefix (for identification) */ | ||
| prefix: string; | ||
| /** Last 4 characters of key */ | ||
| lastFour: string; | ||
| /** Permissions granted */ | ||
| permissions: string[]; | ||
| /** When key was created (ISO 8601) */ | ||
| createdAt: string; | ||
| /** When key was last used (ISO 8601) */ | ||
| lastUsedAt?: string | null; | ||
| /** When key expires (ISO 8601) */ | ||
| expiresAt?: string | null; | ||
| /** Whether key is revoked */ | ||
| isRevoked: boolean; | ||
| } | ||
| /** | ||
| * Test phone numbers for sandbox mode | ||
@@ -813,2 +1054,327 @@ */ | ||
| /** | ||
| * Webhooks Resource | ||
| * @packageDocumentation | ||
| */ | ||
| /** | ||
| * Webhooks API resource | ||
| * | ||
| * Manage webhook endpoints for receiving real-time message status updates. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Create a webhook | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks/sendly', | ||
| * events: ['message.delivered', 'message.failed'] | ||
| * }); | ||
| * | ||
| * // IMPORTANT: Save the secret - it's only shown once! | ||
| * console.log('Secret:', webhook.secret); | ||
| * | ||
| * // List webhooks | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * // Test a webhook | ||
| * const result = await sendly.webhooks.test(webhook.id); | ||
| * ``` | ||
| */ | ||
| declare class WebhooksResource { | ||
| private readonly http; | ||
| constructor(http: HttpClient); | ||
| /** | ||
| * Create a new webhook endpoint | ||
| * | ||
| * @param options - Webhook configuration | ||
| * @returns The created webhook with signing secret (shown only once!) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks/sendly', | ||
| * events: ['message.delivered', 'message.failed'], | ||
| * description: 'Production webhook' | ||
| * }); | ||
| * | ||
| * // IMPORTANT: Save this secret securely - it's only shown once! | ||
| * console.log('Webhook secret:', webhook.secret); | ||
| * ``` | ||
| * | ||
| * @throws {ValidationError} If the URL is invalid or events are empty | ||
| * @throws {AuthenticationError} If the API key is invalid | ||
| */ | ||
| create(options: CreateWebhookOptions): Promise<WebhookCreatedResponse>; | ||
| /** | ||
| * List all webhooks | ||
| * | ||
| * @returns Array of webhook configurations | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * for (const webhook of webhooks) { | ||
| * console.log(`${webhook.id}: ${webhook.url} (${webhook.isActive ? 'active' : 'inactive'})`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| list(): Promise<Webhook[]>; | ||
| /** | ||
| * Get a specific webhook by ID | ||
| * | ||
| * @param id - Webhook ID (whk_xxx) | ||
| * @returns The webhook details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhook = await sendly.webhooks.get('whk_xxx'); | ||
| * console.log(`Success rate: ${webhook.successRate}%`); | ||
| * ``` | ||
| * | ||
| * @throws {NotFoundError} If the webhook doesn't exist | ||
| */ | ||
| get(id: string): Promise<Webhook>; | ||
| /** | ||
| * Update a webhook configuration | ||
| * | ||
| * @param id - Webhook ID | ||
| * @param options - Fields to update | ||
| * @returns The updated webhook | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Update URL | ||
| * await sendly.webhooks.update('whk_xxx', { | ||
| * url: 'https://new-endpoint.example.com/webhooks' | ||
| * }); | ||
| * | ||
| * // Disable webhook | ||
| * await sendly.webhooks.update('whk_xxx', { isActive: false }); | ||
| * | ||
| * // Change event subscriptions | ||
| * await sendly.webhooks.update('whk_xxx', { | ||
| * events: ['message.delivered'] | ||
| * }); | ||
| * ``` | ||
| */ | ||
| update(id: string, options: UpdateWebhookOptions): Promise<Webhook>; | ||
| /** | ||
| * Delete a webhook | ||
| * | ||
| * @param id - Webhook ID | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await sendly.webhooks.delete('whk_xxx'); | ||
| * ``` | ||
| * | ||
| * @throws {NotFoundError} If the webhook doesn't exist | ||
| */ | ||
| delete(id: string): Promise<void>; | ||
| /** | ||
| * Send a test event to a webhook endpoint | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns Test result with response details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const result = await sendly.webhooks.test('whk_xxx'); | ||
| * | ||
| * if (result.success) { | ||
| * console.log(`Test passed! Response time: ${result.responseTimeMs}ms`); | ||
| * } else { | ||
| * console.log(`Test failed: ${result.error}`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| test(id: string): Promise<WebhookTestResult>; | ||
| /** | ||
| * Rotate the webhook signing secret | ||
| * | ||
| * The old secret remains valid for 24 hours to allow for graceful migration. | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns New secret and expiration info | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const rotation = await sendly.webhooks.rotateSecret('whk_xxx'); | ||
| * | ||
| * // Update your webhook handler with the new secret | ||
| * console.log('New secret:', rotation.newSecret); | ||
| * console.log('Old secret expires:', rotation.oldSecretExpiresAt); | ||
| * ``` | ||
| */ | ||
| rotateSecret(id: string): Promise<WebhookSecretRotation>; | ||
| /** | ||
| * Get delivery history for a webhook | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns Array of delivery attempts | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const deliveries = await sendly.webhooks.getDeliveries('whk_xxx'); | ||
| * | ||
| * for (const delivery of deliveries) { | ||
| * console.log(`${delivery.eventType}: ${delivery.status} (${delivery.responseTimeMs}ms)`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| getDeliveries(id: string): Promise<WebhookDelivery[]>; | ||
| /** | ||
| * Retry a failed delivery | ||
| * | ||
| * @param webhookId - Webhook ID | ||
| * @param deliveryId - Delivery ID | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await sendly.webhooks.retryDelivery('whk_xxx', 'del_yyy'); | ||
| * ``` | ||
| */ | ||
| retryDelivery(webhookId: string, deliveryId: string): Promise<void>; | ||
| /** | ||
| * List available event types | ||
| * | ||
| * @returns Array of event type strings | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const eventTypes = await sendly.webhooks.listEventTypes(); | ||
| * console.log('Available events:', eventTypes); | ||
| * // ['message.sent', 'message.delivered', 'message.failed', 'message.bounced'] | ||
| * ``` | ||
| */ | ||
| listEventTypes(): Promise<WebhookEventType[]>; | ||
| } | ||
| /** | ||
| * Account Resource | ||
| * @packageDocumentation | ||
| */ | ||
| /** | ||
| * Account API resource | ||
| * | ||
| * Access account information, credit balance, and API keys. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Get credit balance | ||
| * const credits = await sendly.account.getCredits(); | ||
| * console.log(`Available: ${credits.availableBalance} credits`); | ||
| * | ||
| * // Get transaction history | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * // List API keys | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * ``` | ||
| */ | ||
| declare class AccountResource { | ||
| private readonly http; | ||
| constructor(http: HttpClient); | ||
| /** | ||
| * Get account information | ||
| * | ||
| * @returns Account details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const account = await sendly.account.get(); | ||
| * console.log(`Account: ${account.email}`); | ||
| * ``` | ||
| */ | ||
| get(): Promise<Account>; | ||
| /** | ||
| * Get credit balance | ||
| * | ||
| * @returns Current credit balance and reserved credits | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const credits = await sendly.account.getCredits(); | ||
| * | ||
| * console.log(`Total balance: ${credits.balance}`); | ||
| * console.log(`Reserved (scheduled): ${credits.reservedBalance}`); | ||
| * console.log(`Available to use: ${credits.availableBalance}`); | ||
| * ``` | ||
| */ | ||
| getCredits(): Promise<Credits>; | ||
| /** | ||
| * Get credit transaction history | ||
| * | ||
| * @param options - Pagination options | ||
| * @returns Array of credit transactions | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * for (const tx of transactions) { | ||
| * const sign = tx.amount > 0 ? '+' : ''; | ||
| * console.log(`${tx.type}: ${sign}${tx.amount} credits - ${tx.description}`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| getCreditTransactions(options?: { | ||
| limit?: number; | ||
| offset?: number; | ||
| }): Promise<CreditTransaction[]>; | ||
| /** | ||
| * List API keys for the account | ||
| * | ||
| * Note: This returns key metadata, not the actual secret keys. | ||
| * | ||
| * @returns Array of API keys | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * | ||
| * for (const key of keys) { | ||
| * console.log(`${key.name}: ${key.prefix}...${key.lastFour} (${key.type})`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| listApiKeys(): Promise<ApiKey[]>; | ||
| /** | ||
| * Get a specific API key by ID | ||
| * | ||
| * @param id - API key ID | ||
| * @returns API key details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const key = await sendly.account.getApiKey('key_xxx'); | ||
| * console.log(`Last used: ${key.lastUsedAt}`); | ||
| * ``` | ||
| */ | ||
| getApiKey(id: string): Promise<ApiKey>; | ||
| /** | ||
| * Get usage statistics for an API key | ||
| * | ||
| * @param id - API key ID | ||
| * @returns Usage statistics | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const usage = await sendly.account.getApiKeyUsage('key_xxx'); | ||
| * console.log(`Messages sent: ${usage.messagesSent}`); | ||
| * ``` | ||
| */ | ||
| getApiKeyUsage(id: string): Promise<{ | ||
| keyId: string; | ||
| messagesSent: number; | ||
| messagesDelivered: number; | ||
| messagesFailed: number; | ||
| creditsUsed: number; | ||
| periodStart: string; | ||
| periodEnd: string; | ||
| }>; | ||
| } | ||
| /** | ||
| * Sendly Client | ||
@@ -866,2 +1432,37 @@ * @packageDocumentation | ||
| readonly messages: MessagesResource; | ||
| /** | ||
| * Webhooks API resource | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Create a webhook | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks', | ||
| * events: ['message.delivered', 'message.failed'] | ||
| * }); | ||
| * | ||
| * // List webhooks | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * // Test a webhook | ||
| * await sendly.webhooks.test('whk_xxx'); | ||
| * ``` | ||
| */ | ||
| readonly webhooks: WebhooksResource; | ||
| /** | ||
| * Account API resource | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Get credit balance | ||
| * const credits = await sendly.account.getCredits(); | ||
| * | ||
| * // Get transaction history | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * // List API keys | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * ``` | ||
| */ | ||
| readonly account: AccountResource; | ||
| private readonly http; | ||
@@ -1029,49 +1630,82 @@ private readonly config; | ||
| */ | ||
| /** | ||
| * Webhook event types | ||
| */ | ||
| type WebhookEventType = "message.queued" | "message.sent" | "message.delivered" | "message.failed" | "message.undelivered"; | ||
| /** | ||
| * Message status in webhook events | ||
| */ | ||
| type WebhookMessageStatus = "queued" | "sent" | "delivered" | "failed" | "undelivered"; | ||
| type WebhookMessageStatus = "queued" | "sent" | "delivered" | "failed"; | ||
| /** | ||
| * Webhook message data payload | ||
| * Message object within webhook payload | ||
| * Matches the structure sent by Sendly servers | ||
| */ | ||
| interface WebhookMessageData { | ||
| /** Unique message identifier */ | ||
| messageId: string; | ||
| interface WebhookMessageObject { | ||
| /** Message ID (msg_xxx) */ | ||
| id: string; | ||
| /** Recipient phone number (E.164 format) */ | ||
| to: string; | ||
| /** Sender phone number or ID */ | ||
| from: string; | ||
| /** Message text content */ | ||
| text: string; | ||
| /** Current message status */ | ||
| status: WebhookMessageStatus; | ||
| /** Destination phone number */ | ||
| to: string; | ||
| /** Sender ID or phone number */ | ||
| from: string; | ||
| /** Error message if failed */ | ||
| error?: string; | ||
| /** Error code if failed */ | ||
| errorCode?: string; | ||
| /** ISO 8601 timestamp when delivered */ | ||
| deliveredAt?: string; | ||
| /** ISO 8601 timestamp when failed */ | ||
| failedAt?: string; | ||
| /** Message direction */ | ||
| direction: "outbound" | "inbound"; | ||
| /** Number of SMS segments */ | ||
| segments: number; | ||
| /** Credits charged */ | ||
| creditsUsed: number; | ||
| /** Credits charged for this message */ | ||
| credits_used: number; | ||
| /** Unix timestamp when message was created */ | ||
| created_at: number; | ||
| /** Unix timestamp when message was delivered (if applicable) */ | ||
| delivered_at?: number; | ||
| /** Error message if status is 'failed' */ | ||
| error?: string; | ||
| /** Custom metadata attached to the message */ | ||
| metadata?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * Webhook event structure | ||
| * Webhook event payload from Sendly | ||
| * This is the exact structure sent to your webhook endpoints | ||
| * | ||
| * @example | ||
| * ```json | ||
| * { | ||
| * "id": "evt_abc123", | ||
| * "type": "message.delivered", | ||
| * "api_version": "2024-01", | ||
| * "created": 1702000000, | ||
| * "livemode": true, | ||
| * "data": { | ||
| * "object": { | ||
| * "id": "msg_xyz789", | ||
| * "to": "+15551234567", | ||
| * "from": "+15559876543", | ||
| * "text": "Hello!", | ||
| * "status": "delivered", | ||
| * "direction": "outbound", | ||
| * "segments": 1, | ||
| * "credits_used": 1, | ||
| * "created_at": 1702000000, | ||
| * "delivered_at": 1702000005 | ||
| * } | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| interface WebhookEvent { | ||
| /** Unique event identifier */ | ||
| /** Unique event identifier (evt_xxx) for idempotency */ | ||
| id: string; | ||
| /** Event type */ | ||
| type: WebhookEventType; | ||
| /** Event data payload */ | ||
| data: WebhookMessageData; | ||
| /** ISO 8601 timestamp when event was created */ | ||
| createdAt: string; | ||
| /** API version that generated this event */ | ||
| apiVersion: string; | ||
| api_version: string; | ||
| /** Unix timestamp (seconds) when event was created */ | ||
| created: number; | ||
| /** Whether this event is from live mode (true) or sandbox (false) */ | ||
| livemode: boolean; | ||
| /** Event data containing the message object */ | ||
| data: { | ||
| /** The message object that triggered this event */ | ||
| object: WebhookMessageObject; | ||
| }; | ||
| } | ||
@@ -1087,5 +1721,12 @@ /** | ||
| * | ||
| * @param payload - Raw request body as string | ||
| * Sendly signs webhooks using HMAC-SHA256. The signature format is: | ||
| * `sha256=<hex_digest>` | ||
| * | ||
| * The signed payload is: `<timestamp>.<json_body>` | ||
| * | ||
| * @param payload - Raw request body as string (JSON) | ||
| * @param signature - X-Sendly-Signature header value | ||
| * @param secret - Your webhook secret from dashboard | ||
| * @param timestamp - X-Sendly-Timestamp header value (optional, for enhanced verification) | ||
| * @param toleranceSeconds - Maximum age of webhook in seconds (default: 300 = 5 minutes) | ||
| * @returns true if signature is valid | ||
@@ -1097,2 +1738,3 @@ * | ||
| * | ||
| * // Basic verification | ||
| * const isValid = verifyWebhookSignature( | ||
@@ -1104,2 +1746,10 @@ * req.body, // raw body string | ||
| * | ||
| * // With timestamp verification (recommended) | ||
| * const isValid = verifyWebhookSignature( | ||
| * req.body, | ||
| * req.headers['x-sendly-signature'], | ||
| * process.env.WEBHOOK_SECRET, | ||
| * req.headers['x-sendly-timestamp'] | ||
| * ); | ||
| * | ||
| * if (!isValid) { | ||
@@ -1110,3 +1760,3 @@ * return res.status(401).send('Invalid signature'); | ||
| */ | ||
| declare function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean; | ||
| declare function verifyWebhookSignature(payload: string, signature: string, secret: string, timestamp?: string, toleranceSeconds?: number): boolean; | ||
| /** | ||
@@ -1118,2 +1768,3 @@ * Parse and verify a webhook event | ||
| * @param secret - Your webhook secret from dashboard | ||
| * @param timestamp - X-Sendly-Timestamp header value (optional) | ||
| * @returns Parsed webhook event | ||
@@ -1130,3 +1781,4 @@ * @throws {WebhookSignatureError} If signature is invalid | ||
| * req.headers['x-sendly-signature'], | ||
| * process.env.WEBHOOK_SECRET | ||
| * process.env.WEBHOOK_SECRET, | ||
| * req.headers['x-sendly-timestamp'] | ||
| * ); | ||
@@ -1136,6 +1788,6 @@ * | ||
| * case 'message.delivered': | ||
| * console.log(`Message ${event.data.messageId} delivered!`); | ||
| * console.log(`Message ${event.data.object.id} delivered!`); | ||
| * break; | ||
| * case 'message.failed': | ||
| * console.log(`Message failed: ${event.data.error}`); | ||
| * console.log(`Message failed: ${event.data.object.error}`); | ||
| * break; | ||
@@ -1151,3 +1803,3 @@ * } | ||
| */ | ||
| declare function parseWebhookEvent(payload: string, signature: string, secret: string): WebhookEvent; | ||
| declare function parseWebhookEvent(payload: string, signature: string, secret: string, timestamp?: string): WebhookEvent; | ||
| /** | ||
@@ -1165,11 +1817,27 @@ * Generate a webhook signature for testing purposes | ||
| * // For testing your webhook handler | ||
| * const timestamp = Math.floor(Date.now() / 1000); | ||
| * const testPayload = JSON.stringify({ | ||
| * id: 'evt_test', | ||
| * type: 'message.delivered', | ||
| * data: { messageId: 'msg_123', status: 'delivered' }, | ||
| * createdAt: new Date().toISOString(), | ||
| * apiVersion: '2025-01-01' | ||
| * api_version: '2024-01', | ||
| * created: timestamp, | ||
| * livemode: false, | ||
| * data: { | ||
| * object: { | ||
| * id: 'msg_123', | ||
| * to: '+15551234567', | ||
| * from: '+15559876543', | ||
| * text: 'Test message', | ||
| * status: 'delivered', | ||
| * direction: 'outbound', | ||
| * segments: 1, | ||
| * credits_used: 1, | ||
| * created_at: timestamp, | ||
| * delivered_at: timestamp | ||
| * } | ||
| * } | ||
| * }); | ||
| * | ||
| * const signature = generateWebhookSignature(testPayload, 'test_secret'); | ||
| * const signedPayload = `${timestamp}.${testPayload}`; | ||
| * const signature = generateWebhookSignature(signedPayload, 'test_secret'); | ||
| * ``` | ||
@@ -1187,7 +1855,21 @@ */ | ||
| * | ||
| * // Verify signature | ||
| * const isValid = webhooks.verify(payload, signature); | ||
| * // In your Express/Fastify/etc handler: | ||
| * app.post('/webhooks/sendly', (req, res) => { | ||
| * try { | ||
| * const event = webhooks.parse( | ||
| * req.body, | ||
| * req.headers['x-sendly-signature'], | ||
| * req.headers['x-sendly-timestamp'] | ||
| * ); | ||
| * | ||
| * // Parse event | ||
| * const event = webhooks.parse(payload, signature); | ||
| * // Handle the event | ||
| * if (event.type === 'message.delivered') { | ||
| * console.log(`Message ${event.data.object.id} delivered!`); | ||
| * } | ||
| * | ||
| * res.status(200).send('OK'); | ||
| * } catch (err) { | ||
| * res.status(401).send('Invalid signature'); | ||
| * } | ||
| * }); | ||
| * ``` | ||
@@ -1206,4 +1888,5 @@ */ | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param timestamp - X-Sendly-Timestamp header (optional) | ||
| */ | ||
| verify(payload: string, signature: string): boolean; | ||
| verify(payload: string, signature: string, timestamp?: string): boolean; | ||
| /** | ||
@@ -1213,11 +1896,36 @@ * Parse and verify a webhook event | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param timestamp - X-Sendly-Timestamp header (optional) | ||
| */ | ||
| parse(payload: string, signature: string): WebhookEvent; | ||
| parse(payload: string, signature: string, timestamp?: string): WebhookEvent; | ||
| /** | ||
| * Generate a signature for testing | ||
| * @param payload - Payload to sign (should include timestamp prefix if using timestamps) | ||
| */ | ||
| sign(payload: string): string; | ||
| /** | ||
| * Verify a webhook signature (static method for backwards compatibility) | ||
| * @param payload - Raw request body | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param secret - Your webhook secret | ||
| */ | ||
| static verifySignature(payload: string, signature: string, secret: string): boolean; | ||
| /** | ||
| * Parse and verify a webhook event (static method for backwards compatibility) | ||
| * @param payload - Raw request body | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param secret - Your webhook secret | ||
| */ | ||
| static parseEvent(payload: string, signature: string, secret: string): WebhookEvent; | ||
| /** | ||
| * Generate a webhook signature (static method for backwards compatibility) | ||
| * @param payload - Payload to sign | ||
| * @param secret - Secret to use for signing | ||
| */ | ||
| sign(payload: string): string; | ||
| static generateSignature(payload: string, secret: string): string; | ||
| } | ||
| /** | ||
| * @deprecated Use WebhookMessageObject instead | ||
| */ | ||
| type WebhookMessageData = WebhookMessageObject; | ||
| export { ALL_SUPPORTED_COUNTRIES, type ApiErrorResponse, AuthenticationError, CREDITS_PER_SMS, InsufficientCreditsError, type ListMessagesOptions, type Message, type MessageListResponse, type MessageStatus, NetworkError, NotFoundError, type PricingTier, RateLimitError, type RateLimitInfo, SANDBOX_TEST_NUMBERS, SUPPORTED_COUNTRIES, type SendMessageRequest, Sendly, type SendlyConfig, SendlyError, type SendlyErrorCode, TimeoutError, ValidationError, type WebhookEvent, type WebhookEventType, type WebhookMessageData, type WebhookMessageStatus, WebhookSignatureError, Webhooks, calculateSegments, Sendly as default, generateWebhookSignature, getCountryFromPhone, isCountrySupported, parseWebhookEvent, validateMessageText, validatePhoneNumber, validateSenderId, verifyWebhookSignature }; | ||
| export { ALL_SUPPORTED_COUNTRIES, type Account, type ApiErrorResponse, type ApiKey, AuthenticationError, type BatchListResponse, type BatchMessageItem, type BatchMessageRequest, type BatchMessageResponse, type BatchMessageResult, type BatchStatus, CREDITS_PER_SMS, type CancelledMessageResponse, type CircuitState, type CreateWebhookOptions, type CreditTransaction, type Credits, type DeliveryStatus, InsufficientCreditsError, type ListBatchesOptions, type ListMessagesOptions, type ListScheduledMessagesOptions, type Message, type MessageListResponse, type MessageStatus, NetworkError, NotFoundError, type PricingTier, RateLimitError, type RateLimitInfo, SANDBOX_TEST_NUMBERS, SUPPORTED_COUNTRIES, type ScheduleMessageRequest, type ScheduledMessage, type ScheduledMessageListResponse, type ScheduledMessageStatus, type SendMessageRequest, type SenderType, Sendly, type SendlyConfig, SendlyError, type SendlyErrorCode, TimeoutError, type UpdateWebhookOptions, ValidationError, type Webhook, type WebhookCreatedResponse, type WebhookDelivery, type WebhookEvent, type WebhookEventType, type WebhookMessageData, type WebhookMessageStatus, type WebhookSecretRotation, WebhookSignatureError, type WebhookTestResult, Webhooks, calculateSegments, Sendly as default, generateWebhookSignature, getCountryFromPhone, isCountrySupported, parseWebhookEvent, validateMessageText, validatePhoneNumber, validateSenderId, verifyWebhookSignature }; |
+548
-10
@@ -981,2 +981,439 @@ "use strict"; | ||
| // src/utils/transform.ts | ||
| function snakeToCamel(str) { | ||
| return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); | ||
| } | ||
| function transformKeys(obj) { | ||
| if (obj === null || obj === void 0) { | ||
| return obj; | ||
| } | ||
| if (Array.isArray(obj)) { | ||
| return obj.map((item) => transformKeys(item)); | ||
| } | ||
| if (typeof obj === "object") { | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| const camelKey = snakeToCamel(key); | ||
| result[camelKey] = transformKeys(value); | ||
| } | ||
| return result; | ||
| } | ||
| return obj; | ||
| } | ||
| // src/resources/webhooks.ts | ||
| var WebhooksResource = class { | ||
| http; | ||
| constructor(http) { | ||
| this.http = http; | ||
| } | ||
| /** | ||
| * Create a new webhook endpoint | ||
| * | ||
| * @param options - Webhook configuration | ||
| * @returns The created webhook with signing secret (shown only once!) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks/sendly', | ||
| * events: ['message.delivered', 'message.failed'], | ||
| * description: 'Production webhook' | ||
| * }); | ||
| * | ||
| * // IMPORTANT: Save this secret securely - it's only shown once! | ||
| * console.log('Webhook secret:', webhook.secret); | ||
| * ``` | ||
| * | ||
| * @throws {ValidationError} If the URL is invalid or events are empty | ||
| * @throws {AuthenticationError} If the API key is invalid | ||
| */ | ||
| async create(options) { | ||
| if (!options.url || !options.url.startsWith("https://")) { | ||
| throw new Error("Webhook URL must be HTTPS"); | ||
| } | ||
| if (!options.events || options.events.length === 0) { | ||
| throw new Error("At least one event type is required"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "POST", | ||
| path: "/webhooks", | ||
| body: { | ||
| url: options.url, | ||
| events: options.events, | ||
| ...options.description && { description: options.description }, | ||
| ...options.metadata && { metadata: options.metadata } | ||
| } | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * List all webhooks | ||
| * | ||
| * @returns Array of webhook configurations | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * for (const webhook of webhooks) { | ||
| * console.log(`${webhook.id}: ${webhook.url} (${webhook.isActive ? 'active' : 'inactive'})`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async list() { | ||
| const response = await this.http.request({ | ||
| method: "GET", | ||
| path: "/webhooks" | ||
| }); | ||
| return response.map((item) => transformKeys(item)); | ||
| } | ||
| /** | ||
| * Get a specific webhook by ID | ||
| * | ||
| * @param id - Webhook ID (whk_xxx) | ||
| * @returns The webhook details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhook = await sendly.webhooks.get('whk_xxx'); | ||
| * console.log(`Success rate: ${webhook.successRate}%`); | ||
| * ``` | ||
| * | ||
| * @throws {NotFoundError} If the webhook doesn't exist | ||
| */ | ||
| async get(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "GET", | ||
| path: `/webhooks/${encodeURIComponent(id)}` | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * Update a webhook configuration | ||
| * | ||
| * @param id - Webhook ID | ||
| * @param options - Fields to update | ||
| * @returns The updated webhook | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Update URL | ||
| * await sendly.webhooks.update('whk_xxx', { | ||
| * url: 'https://new-endpoint.example.com/webhooks' | ||
| * }); | ||
| * | ||
| * // Disable webhook | ||
| * await sendly.webhooks.update('whk_xxx', { isActive: false }); | ||
| * | ||
| * // Change event subscriptions | ||
| * await sendly.webhooks.update('whk_xxx', { | ||
| * events: ['message.delivered'] | ||
| * }); | ||
| * ``` | ||
| */ | ||
| async update(id, options) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| if (options.url && !options.url.startsWith("https://")) { | ||
| throw new Error("Webhook URL must be HTTPS"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "PATCH", | ||
| path: `/webhooks/${encodeURIComponent(id)}`, | ||
| body: { | ||
| ...options.url !== void 0 && { url: options.url }, | ||
| ...options.events !== void 0 && { events: options.events }, | ||
| ...options.description !== void 0 && { | ||
| description: options.description | ||
| }, | ||
| ...options.isActive !== void 0 && { is_active: options.isActive }, | ||
| ...options.metadata !== void 0 && { metadata: options.metadata } | ||
| } | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * Delete a webhook | ||
| * | ||
| * @param id - Webhook ID | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await sendly.webhooks.delete('whk_xxx'); | ||
| * ``` | ||
| * | ||
| * @throws {NotFoundError} If the webhook doesn't exist | ||
| */ | ||
| async delete(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| await this.http.request({ | ||
| method: "DELETE", | ||
| path: `/webhooks/${encodeURIComponent(id)}` | ||
| }); | ||
| } | ||
| /** | ||
| * Send a test event to a webhook endpoint | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns Test result with response details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const result = await sendly.webhooks.test('whk_xxx'); | ||
| * | ||
| * if (result.success) { | ||
| * console.log(`Test passed! Response time: ${result.responseTimeMs}ms`); | ||
| * } else { | ||
| * console.log(`Test failed: ${result.error}`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async test(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "POST", | ||
| path: `/webhooks/${encodeURIComponent(id)}/test` | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * Rotate the webhook signing secret | ||
| * | ||
| * The old secret remains valid for 24 hours to allow for graceful migration. | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns New secret and expiration info | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const rotation = await sendly.webhooks.rotateSecret('whk_xxx'); | ||
| * | ||
| * // Update your webhook handler with the new secret | ||
| * console.log('New secret:', rotation.newSecret); | ||
| * console.log('Old secret expires:', rotation.oldSecretExpiresAt); | ||
| * ``` | ||
| */ | ||
| async rotateSecret(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "POST", | ||
| path: `/webhooks/${encodeURIComponent(id)}/rotate-secret` | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * Get delivery history for a webhook | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns Array of delivery attempts | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const deliveries = await sendly.webhooks.getDeliveries('whk_xxx'); | ||
| * | ||
| * for (const delivery of deliveries) { | ||
| * console.log(`${delivery.eventType}: ${delivery.status} (${delivery.responseTimeMs}ms)`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async getDeliveries(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "GET", | ||
| path: `/webhooks/${encodeURIComponent(id)}/deliveries` | ||
| }); | ||
| return response.map((item) => transformKeys(item)); | ||
| } | ||
| /** | ||
| * Retry a failed delivery | ||
| * | ||
| * @param webhookId - Webhook ID | ||
| * @param deliveryId - Delivery ID | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await sendly.webhooks.retryDelivery('whk_xxx', 'del_yyy'); | ||
| * ``` | ||
| */ | ||
| async retryDelivery(webhookId, deliveryId) { | ||
| if (!webhookId || !webhookId.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| if (!deliveryId || !deliveryId.startsWith("del_")) { | ||
| throw new Error("Invalid delivery ID format"); | ||
| } | ||
| await this.http.request({ | ||
| method: "POST", | ||
| path: `/webhooks/${encodeURIComponent(webhookId)}/deliveries/${encodeURIComponent(deliveryId)}/retry` | ||
| }); | ||
| } | ||
| /** | ||
| * List available event types | ||
| * | ||
| * @returns Array of event type strings | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const eventTypes = await sendly.webhooks.listEventTypes(); | ||
| * console.log('Available events:', eventTypes); | ||
| * // ['message.sent', 'message.delivered', 'message.failed', 'message.bounced'] | ||
| * ``` | ||
| */ | ||
| async listEventTypes() { | ||
| const eventTypes = await this.http.request({ | ||
| method: "GET", | ||
| path: "/webhooks/event-types" | ||
| }); | ||
| return eventTypes; | ||
| } | ||
| }; | ||
| // src/resources/account.ts | ||
| var AccountResource = class { | ||
| http; | ||
| constructor(http) { | ||
| this.http = http; | ||
| } | ||
| /** | ||
| * Get account information | ||
| * | ||
| * @returns Account details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const account = await sendly.account.get(); | ||
| * console.log(`Account: ${account.email}`); | ||
| * ``` | ||
| */ | ||
| async get() { | ||
| const account = await this.http.request({ | ||
| method: "GET", | ||
| path: "/account" | ||
| }); | ||
| return account; | ||
| } | ||
| /** | ||
| * Get credit balance | ||
| * | ||
| * @returns Current credit balance and reserved credits | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const credits = await sendly.account.getCredits(); | ||
| * | ||
| * console.log(`Total balance: ${credits.balance}`); | ||
| * console.log(`Reserved (scheduled): ${credits.reservedBalance}`); | ||
| * console.log(`Available to use: ${credits.availableBalance}`); | ||
| * ``` | ||
| */ | ||
| async getCredits() { | ||
| const credits = await this.http.request({ | ||
| method: "GET", | ||
| path: "/credits" | ||
| }); | ||
| return credits; | ||
| } | ||
| /** | ||
| * Get credit transaction history | ||
| * | ||
| * @param options - Pagination options | ||
| * @returns Array of credit transactions | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * for (const tx of transactions) { | ||
| * const sign = tx.amount > 0 ? '+' : ''; | ||
| * console.log(`${tx.type}: ${sign}${tx.amount} credits - ${tx.description}`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async getCreditTransactions(options) { | ||
| const transactions = await this.http.request({ | ||
| method: "GET", | ||
| path: "/credits/transactions", | ||
| query: { | ||
| limit: options?.limit, | ||
| offset: options?.offset | ||
| } | ||
| }); | ||
| return transactions; | ||
| } | ||
| /** | ||
| * List API keys for the account | ||
| * | ||
| * Note: This returns key metadata, not the actual secret keys. | ||
| * | ||
| * @returns Array of API keys | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * | ||
| * for (const key of keys) { | ||
| * console.log(`${key.name}: ${key.prefix}...${key.lastFour} (${key.type})`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async listApiKeys() { | ||
| const keys = await this.http.request({ | ||
| method: "GET", | ||
| path: "/keys" | ||
| }); | ||
| return keys; | ||
| } | ||
| /** | ||
| * Get a specific API key by ID | ||
| * | ||
| * @param id - API key ID | ||
| * @returns API key details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const key = await sendly.account.getApiKey('key_xxx'); | ||
| * console.log(`Last used: ${key.lastUsedAt}`); | ||
| * ``` | ||
| */ | ||
| async getApiKey(id) { | ||
| const key = await this.http.request({ | ||
| method: "GET", | ||
| path: `/keys/${encodeURIComponent(id)}` | ||
| }); | ||
| return key; | ||
| } | ||
| /** | ||
| * Get usage statistics for an API key | ||
| * | ||
| * @param id - API key ID | ||
| * @returns Usage statistics | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const usage = await sendly.account.getApiKeyUsage('key_xxx'); | ||
| * console.log(`Messages sent: ${usage.messagesSent}`); | ||
| * ``` | ||
| */ | ||
| async getApiKeyUsage(id) { | ||
| const usage = await this.http.request({ | ||
| method: "GET", | ||
| path: `/keys/${encodeURIComponent(id)}/usage` | ||
| }); | ||
| return usage; | ||
| } | ||
| }; | ||
| // src/client.ts | ||
@@ -1003,2 +1440,37 @@ var DEFAULT_BASE_URL2 = "https://sendly.live/api/v1"; | ||
| messages; | ||
| /** | ||
| * Webhooks API resource | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Create a webhook | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks', | ||
| * events: ['message.delivered', 'message.failed'] | ||
| * }); | ||
| * | ||
| * // List webhooks | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * // Test a webhook | ||
| * await sendly.webhooks.test('whk_xxx'); | ||
| * ``` | ||
| */ | ||
| webhooks; | ||
| /** | ||
| * Account API resource | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Get credit balance | ||
| * const credits = await sendly.account.getCredits(); | ||
| * | ||
| * // Get transaction history | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * // List API keys | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * ``` | ||
| */ | ||
| account; | ||
| http; | ||
@@ -1034,2 +1506,4 @@ config; | ||
| this.messages = new MessagesResource(this.http); | ||
| this.webhooks = new WebhooksResource(this.http); | ||
| this.account = new AccountResource(this.http); | ||
| } | ||
@@ -1088,7 +1562,18 @@ /** | ||
| }; | ||
| function verifyWebhookSignature(payload, signature, secret) { | ||
| function verifyWebhookSignature(payload, signature, secret, timestamp, toleranceSeconds = 300) { | ||
| if (!payload || !signature || !secret) { | ||
| return false; | ||
| } | ||
| const expectedSignature = generateWebhookSignature(payload, secret); | ||
| if (timestamp) { | ||
| const timestampNum = parseInt(timestamp, 10); | ||
| if (isNaN(timestampNum)) { | ||
| return false; | ||
| } | ||
| const now = Math.floor(Date.now() / 1e3); | ||
| if (Math.abs(now - timestampNum) > toleranceSeconds) { | ||
| return false; | ||
| } | ||
| } | ||
| const signedPayload = timestamp ? `${timestamp}.${payload}` : payload; | ||
| const expectedSignature = generateWebhookSignature(signedPayload, secret); | ||
| try { | ||
@@ -1103,4 +1588,4 @@ return crypto.timingSafeEqual( | ||
| } | ||
| function parseWebhookEvent(payload, signature, secret) { | ||
| if (!verifyWebhookSignature(payload, signature, secret)) { | ||
| function parseWebhookEvent(payload, signature, secret, timestamp) { | ||
| if (!verifyWebhookSignature(payload, signature, secret, timestamp)) { | ||
| throw new WebhookSignatureError(); | ||
@@ -1114,3 +1599,3 @@ } | ||
| } | ||
| if (!event.id || !event.type || !event.createdAt) { | ||
| if (!event.id || !event.type || !event.created || !event.data?.object) { | ||
| throw new Error("Invalid webhook event structure"); | ||
@@ -1141,5 +1626,6 @@ } | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param timestamp - X-Sendly-Timestamp header (optional) | ||
| */ | ||
| verify(payload, signature) { | ||
| return verifyWebhookSignature(payload, signature, this.secret); | ||
| verify(payload, signature, timestamp) { | ||
| return verifyWebhookSignature(payload, signature, this.secret, timestamp); | ||
| } | ||
@@ -1150,9 +1636,10 @@ /** | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param timestamp - X-Sendly-Timestamp header (optional) | ||
| */ | ||
| parse(payload, signature) { | ||
| return parseWebhookEvent(payload, signature, this.secret); | ||
| parse(payload, signature, timestamp) { | ||
| return parseWebhookEvent(payload, signature, this.secret, timestamp); | ||
| } | ||
| /** | ||
| * Generate a signature for testing | ||
| * @param payload - Payload to sign | ||
| * @param payload - Payload to sign (should include timestamp prefix if using timestamps) | ||
| */ | ||
@@ -1162,2 +1649,53 @@ sign(payload) { | ||
| } | ||
| // ============================================================================ | ||
| // Static methods for backwards compatibility with existing code/tests | ||
| // ============================================================================ | ||
| /** | ||
| * Verify a webhook signature (static method for backwards compatibility) | ||
| * @param payload - Raw request body | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param secret - Your webhook secret | ||
| */ | ||
| static verifySignature(payload, signature, secret) { | ||
| return verifyWebhookSignature(payload, signature, secret); | ||
| } | ||
| /** | ||
| * Parse and verify a webhook event (static method for backwards compatibility) | ||
| * @param payload - Raw request body | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param secret - Your webhook secret | ||
| */ | ||
| static parseEvent(payload, signature, secret) { | ||
| if (!verifyWebhookSignature(payload, signature, secret)) { | ||
| throw new WebhookSignatureError("Invalid webhook signature"); | ||
| } | ||
| let event; | ||
| try { | ||
| event = JSON.parse(payload); | ||
| } catch { | ||
| throw new WebhookSignatureError("Failed to parse webhook payload"); | ||
| } | ||
| const parsed = event; | ||
| if (!parsed.id || !parsed.type || !parsed.created_at) { | ||
| throw new WebhookSignatureError("Invalid event structure"); | ||
| } | ||
| if (parsed.data && typeof parsed.data === "object" && "message_id" in parsed.data) { | ||
| return event; | ||
| } | ||
| if (parsed.data && typeof parsed.data === "object" && "object" in parsed.data) { | ||
| return event; | ||
| } | ||
| if (!parsed.data) { | ||
| throw new WebhookSignatureError("Invalid event structure"); | ||
| } | ||
| return event; | ||
| } | ||
| /** | ||
| * Generate a webhook signature (static method for backwards compatibility) | ||
| * @param payload - Payload to sign | ||
| * @param secret - Secret to use for signing | ||
| */ | ||
| static generateSignature(payload, secret) { | ||
| return generateWebhookSignature(payload, secret); | ||
| } | ||
| }; | ||
@@ -1164,0 +1702,0 @@ // Annotate the CommonJS export names for ESM import in node: |
+548
-10
@@ -921,2 +921,439 @@ // src/errors.ts | ||
| // src/utils/transform.ts | ||
| function snakeToCamel(str) { | ||
| return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); | ||
| } | ||
| function transformKeys(obj) { | ||
| if (obj === null || obj === void 0) { | ||
| return obj; | ||
| } | ||
| if (Array.isArray(obj)) { | ||
| return obj.map((item) => transformKeys(item)); | ||
| } | ||
| if (typeof obj === "object") { | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| const camelKey = snakeToCamel(key); | ||
| result[camelKey] = transformKeys(value); | ||
| } | ||
| return result; | ||
| } | ||
| return obj; | ||
| } | ||
| // src/resources/webhooks.ts | ||
| var WebhooksResource = class { | ||
| http; | ||
| constructor(http) { | ||
| this.http = http; | ||
| } | ||
| /** | ||
| * Create a new webhook endpoint | ||
| * | ||
| * @param options - Webhook configuration | ||
| * @returns The created webhook with signing secret (shown only once!) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks/sendly', | ||
| * events: ['message.delivered', 'message.failed'], | ||
| * description: 'Production webhook' | ||
| * }); | ||
| * | ||
| * // IMPORTANT: Save this secret securely - it's only shown once! | ||
| * console.log('Webhook secret:', webhook.secret); | ||
| * ``` | ||
| * | ||
| * @throws {ValidationError} If the URL is invalid or events are empty | ||
| * @throws {AuthenticationError} If the API key is invalid | ||
| */ | ||
| async create(options) { | ||
| if (!options.url || !options.url.startsWith("https://")) { | ||
| throw new Error("Webhook URL must be HTTPS"); | ||
| } | ||
| if (!options.events || options.events.length === 0) { | ||
| throw new Error("At least one event type is required"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "POST", | ||
| path: "/webhooks", | ||
| body: { | ||
| url: options.url, | ||
| events: options.events, | ||
| ...options.description && { description: options.description }, | ||
| ...options.metadata && { metadata: options.metadata } | ||
| } | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * List all webhooks | ||
| * | ||
| * @returns Array of webhook configurations | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * for (const webhook of webhooks) { | ||
| * console.log(`${webhook.id}: ${webhook.url} (${webhook.isActive ? 'active' : 'inactive'})`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async list() { | ||
| const response = await this.http.request({ | ||
| method: "GET", | ||
| path: "/webhooks" | ||
| }); | ||
| return response.map((item) => transformKeys(item)); | ||
| } | ||
| /** | ||
| * Get a specific webhook by ID | ||
| * | ||
| * @param id - Webhook ID (whk_xxx) | ||
| * @returns The webhook details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const webhook = await sendly.webhooks.get('whk_xxx'); | ||
| * console.log(`Success rate: ${webhook.successRate}%`); | ||
| * ``` | ||
| * | ||
| * @throws {NotFoundError} If the webhook doesn't exist | ||
| */ | ||
| async get(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "GET", | ||
| path: `/webhooks/${encodeURIComponent(id)}` | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * Update a webhook configuration | ||
| * | ||
| * @param id - Webhook ID | ||
| * @param options - Fields to update | ||
| * @returns The updated webhook | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Update URL | ||
| * await sendly.webhooks.update('whk_xxx', { | ||
| * url: 'https://new-endpoint.example.com/webhooks' | ||
| * }); | ||
| * | ||
| * // Disable webhook | ||
| * await sendly.webhooks.update('whk_xxx', { isActive: false }); | ||
| * | ||
| * // Change event subscriptions | ||
| * await sendly.webhooks.update('whk_xxx', { | ||
| * events: ['message.delivered'] | ||
| * }); | ||
| * ``` | ||
| */ | ||
| async update(id, options) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| if (options.url && !options.url.startsWith("https://")) { | ||
| throw new Error("Webhook URL must be HTTPS"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "PATCH", | ||
| path: `/webhooks/${encodeURIComponent(id)}`, | ||
| body: { | ||
| ...options.url !== void 0 && { url: options.url }, | ||
| ...options.events !== void 0 && { events: options.events }, | ||
| ...options.description !== void 0 && { | ||
| description: options.description | ||
| }, | ||
| ...options.isActive !== void 0 && { is_active: options.isActive }, | ||
| ...options.metadata !== void 0 && { metadata: options.metadata } | ||
| } | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * Delete a webhook | ||
| * | ||
| * @param id - Webhook ID | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await sendly.webhooks.delete('whk_xxx'); | ||
| * ``` | ||
| * | ||
| * @throws {NotFoundError} If the webhook doesn't exist | ||
| */ | ||
| async delete(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| await this.http.request({ | ||
| method: "DELETE", | ||
| path: `/webhooks/${encodeURIComponent(id)}` | ||
| }); | ||
| } | ||
| /** | ||
| * Send a test event to a webhook endpoint | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns Test result with response details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const result = await sendly.webhooks.test('whk_xxx'); | ||
| * | ||
| * if (result.success) { | ||
| * console.log(`Test passed! Response time: ${result.responseTimeMs}ms`); | ||
| * } else { | ||
| * console.log(`Test failed: ${result.error}`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async test(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "POST", | ||
| path: `/webhooks/${encodeURIComponent(id)}/test` | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * Rotate the webhook signing secret | ||
| * | ||
| * The old secret remains valid for 24 hours to allow for graceful migration. | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns New secret and expiration info | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const rotation = await sendly.webhooks.rotateSecret('whk_xxx'); | ||
| * | ||
| * // Update your webhook handler with the new secret | ||
| * console.log('New secret:', rotation.newSecret); | ||
| * console.log('Old secret expires:', rotation.oldSecretExpiresAt); | ||
| * ``` | ||
| */ | ||
| async rotateSecret(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "POST", | ||
| path: `/webhooks/${encodeURIComponent(id)}/rotate-secret` | ||
| }); | ||
| return transformKeys(response); | ||
| } | ||
| /** | ||
| * Get delivery history for a webhook | ||
| * | ||
| * @param id - Webhook ID | ||
| * @returns Array of delivery attempts | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const deliveries = await sendly.webhooks.getDeliveries('whk_xxx'); | ||
| * | ||
| * for (const delivery of deliveries) { | ||
| * console.log(`${delivery.eventType}: ${delivery.status} (${delivery.responseTimeMs}ms)`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async getDeliveries(id) { | ||
| if (!id || !id.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| const response = await this.http.request({ | ||
| method: "GET", | ||
| path: `/webhooks/${encodeURIComponent(id)}/deliveries` | ||
| }); | ||
| return response.map((item) => transformKeys(item)); | ||
| } | ||
| /** | ||
| * Retry a failed delivery | ||
| * | ||
| * @param webhookId - Webhook ID | ||
| * @param deliveryId - Delivery ID | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await sendly.webhooks.retryDelivery('whk_xxx', 'del_yyy'); | ||
| * ``` | ||
| */ | ||
| async retryDelivery(webhookId, deliveryId) { | ||
| if (!webhookId || !webhookId.startsWith("whk_")) { | ||
| throw new Error("Invalid webhook ID format"); | ||
| } | ||
| if (!deliveryId || !deliveryId.startsWith("del_")) { | ||
| throw new Error("Invalid delivery ID format"); | ||
| } | ||
| await this.http.request({ | ||
| method: "POST", | ||
| path: `/webhooks/${encodeURIComponent(webhookId)}/deliveries/${encodeURIComponent(deliveryId)}/retry` | ||
| }); | ||
| } | ||
| /** | ||
| * List available event types | ||
| * | ||
| * @returns Array of event type strings | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const eventTypes = await sendly.webhooks.listEventTypes(); | ||
| * console.log('Available events:', eventTypes); | ||
| * // ['message.sent', 'message.delivered', 'message.failed', 'message.bounced'] | ||
| * ``` | ||
| */ | ||
| async listEventTypes() { | ||
| const eventTypes = await this.http.request({ | ||
| method: "GET", | ||
| path: "/webhooks/event-types" | ||
| }); | ||
| return eventTypes; | ||
| } | ||
| }; | ||
| // src/resources/account.ts | ||
| var AccountResource = class { | ||
| http; | ||
| constructor(http) { | ||
| this.http = http; | ||
| } | ||
| /** | ||
| * Get account information | ||
| * | ||
| * @returns Account details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const account = await sendly.account.get(); | ||
| * console.log(`Account: ${account.email}`); | ||
| * ``` | ||
| */ | ||
| async get() { | ||
| const account = await this.http.request({ | ||
| method: "GET", | ||
| path: "/account" | ||
| }); | ||
| return account; | ||
| } | ||
| /** | ||
| * Get credit balance | ||
| * | ||
| * @returns Current credit balance and reserved credits | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const credits = await sendly.account.getCredits(); | ||
| * | ||
| * console.log(`Total balance: ${credits.balance}`); | ||
| * console.log(`Reserved (scheduled): ${credits.reservedBalance}`); | ||
| * console.log(`Available to use: ${credits.availableBalance}`); | ||
| * ``` | ||
| */ | ||
| async getCredits() { | ||
| const credits = await this.http.request({ | ||
| method: "GET", | ||
| path: "/credits" | ||
| }); | ||
| return credits; | ||
| } | ||
| /** | ||
| * Get credit transaction history | ||
| * | ||
| * @param options - Pagination options | ||
| * @returns Array of credit transactions | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * for (const tx of transactions) { | ||
| * const sign = tx.amount > 0 ? '+' : ''; | ||
| * console.log(`${tx.type}: ${sign}${tx.amount} credits - ${tx.description}`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async getCreditTransactions(options) { | ||
| const transactions = await this.http.request({ | ||
| method: "GET", | ||
| path: "/credits/transactions", | ||
| query: { | ||
| limit: options?.limit, | ||
| offset: options?.offset | ||
| } | ||
| }); | ||
| return transactions; | ||
| } | ||
| /** | ||
| * List API keys for the account | ||
| * | ||
| * Note: This returns key metadata, not the actual secret keys. | ||
| * | ||
| * @returns Array of API keys | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * | ||
| * for (const key of keys) { | ||
| * console.log(`${key.name}: ${key.prefix}...${key.lastFour} (${key.type})`); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async listApiKeys() { | ||
| const keys = await this.http.request({ | ||
| method: "GET", | ||
| path: "/keys" | ||
| }); | ||
| return keys; | ||
| } | ||
| /** | ||
| * Get a specific API key by ID | ||
| * | ||
| * @param id - API key ID | ||
| * @returns API key details | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const key = await sendly.account.getApiKey('key_xxx'); | ||
| * console.log(`Last used: ${key.lastUsedAt}`); | ||
| * ``` | ||
| */ | ||
| async getApiKey(id) { | ||
| const key = await this.http.request({ | ||
| method: "GET", | ||
| path: `/keys/${encodeURIComponent(id)}` | ||
| }); | ||
| return key; | ||
| } | ||
| /** | ||
| * Get usage statistics for an API key | ||
| * | ||
| * @param id - API key ID | ||
| * @returns Usage statistics | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const usage = await sendly.account.getApiKeyUsage('key_xxx'); | ||
| * console.log(`Messages sent: ${usage.messagesSent}`); | ||
| * ``` | ||
| */ | ||
| async getApiKeyUsage(id) { | ||
| const usage = await this.http.request({ | ||
| method: "GET", | ||
| path: `/keys/${encodeURIComponent(id)}/usage` | ||
| }); | ||
| return usage; | ||
| } | ||
| }; | ||
| // src/client.ts | ||
@@ -943,2 +1380,37 @@ var DEFAULT_BASE_URL2 = "https://sendly.live/api/v1"; | ||
| messages; | ||
| /** | ||
| * Webhooks API resource | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Create a webhook | ||
| * const webhook = await sendly.webhooks.create({ | ||
| * url: 'https://example.com/webhooks', | ||
| * events: ['message.delivered', 'message.failed'] | ||
| * }); | ||
| * | ||
| * // List webhooks | ||
| * const webhooks = await sendly.webhooks.list(); | ||
| * | ||
| * // Test a webhook | ||
| * await sendly.webhooks.test('whk_xxx'); | ||
| * ``` | ||
| */ | ||
| webhooks; | ||
| /** | ||
| * Account API resource | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Get credit balance | ||
| * const credits = await sendly.account.getCredits(); | ||
| * | ||
| * // Get transaction history | ||
| * const transactions = await sendly.account.getCreditTransactions(); | ||
| * | ||
| * // List API keys | ||
| * const keys = await sendly.account.listApiKeys(); | ||
| * ``` | ||
| */ | ||
| account; | ||
| http; | ||
@@ -974,2 +1446,4 @@ config; | ||
| this.messages = new MessagesResource(this.http); | ||
| this.webhooks = new WebhooksResource(this.http); | ||
| this.account = new AccountResource(this.http); | ||
| } | ||
@@ -1028,7 +1502,18 @@ /** | ||
| }; | ||
| function verifyWebhookSignature(payload, signature, secret) { | ||
| function verifyWebhookSignature(payload, signature, secret, timestamp, toleranceSeconds = 300) { | ||
| if (!payload || !signature || !secret) { | ||
| return false; | ||
| } | ||
| const expectedSignature = generateWebhookSignature(payload, secret); | ||
| if (timestamp) { | ||
| const timestampNum = parseInt(timestamp, 10); | ||
| if (isNaN(timestampNum)) { | ||
| return false; | ||
| } | ||
| const now = Math.floor(Date.now() / 1e3); | ||
| if (Math.abs(now - timestampNum) > toleranceSeconds) { | ||
| return false; | ||
| } | ||
| } | ||
| const signedPayload = timestamp ? `${timestamp}.${payload}` : payload; | ||
| const expectedSignature = generateWebhookSignature(signedPayload, secret); | ||
| try { | ||
@@ -1043,4 +1528,4 @@ return crypto.timingSafeEqual( | ||
| } | ||
| function parseWebhookEvent(payload, signature, secret) { | ||
| if (!verifyWebhookSignature(payload, signature, secret)) { | ||
| function parseWebhookEvent(payload, signature, secret, timestamp) { | ||
| if (!verifyWebhookSignature(payload, signature, secret, timestamp)) { | ||
| throw new WebhookSignatureError(); | ||
@@ -1054,3 +1539,3 @@ } | ||
| } | ||
| if (!event.id || !event.type || !event.createdAt) { | ||
| if (!event.id || !event.type || !event.created || !event.data?.object) { | ||
| throw new Error("Invalid webhook event structure"); | ||
@@ -1081,5 +1566,6 @@ } | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param timestamp - X-Sendly-Timestamp header (optional) | ||
| */ | ||
| verify(payload, signature) { | ||
| return verifyWebhookSignature(payload, signature, this.secret); | ||
| verify(payload, signature, timestamp) { | ||
| return verifyWebhookSignature(payload, signature, this.secret, timestamp); | ||
| } | ||
@@ -1090,9 +1576,10 @@ /** | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param timestamp - X-Sendly-Timestamp header (optional) | ||
| */ | ||
| parse(payload, signature) { | ||
| return parseWebhookEvent(payload, signature, this.secret); | ||
| parse(payload, signature, timestamp) { | ||
| return parseWebhookEvent(payload, signature, this.secret, timestamp); | ||
| } | ||
| /** | ||
| * Generate a signature for testing | ||
| * @param payload - Payload to sign | ||
| * @param payload - Payload to sign (should include timestamp prefix if using timestamps) | ||
| */ | ||
@@ -1102,2 +1589,53 @@ sign(payload) { | ||
| } | ||
| // ============================================================================ | ||
| // Static methods for backwards compatibility with existing code/tests | ||
| // ============================================================================ | ||
| /** | ||
| * Verify a webhook signature (static method for backwards compatibility) | ||
| * @param payload - Raw request body | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param secret - Your webhook secret | ||
| */ | ||
| static verifySignature(payload, signature, secret) { | ||
| return verifyWebhookSignature(payload, signature, secret); | ||
| } | ||
| /** | ||
| * Parse and verify a webhook event (static method for backwards compatibility) | ||
| * @param payload - Raw request body | ||
| * @param signature - X-Sendly-Signature header | ||
| * @param secret - Your webhook secret | ||
| */ | ||
| static parseEvent(payload, signature, secret) { | ||
| if (!verifyWebhookSignature(payload, signature, secret)) { | ||
| throw new WebhookSignatureError("Invalid webhook signature"); | ||
| } | ||
| let event; | ||
| try { | ||
| event = JSON.parse(payload); | ||
| } catch { | ||
| throw new WebhookSignatureError("Failed to parse webhook payload"); | ||
| } | ||
| const parsed = event; | ||
| if (!parsed.id || !parsed.type || !parsed.created_at) { | ||
| throw new WebhookSignatureError("Invalid event structure"); | ||
| } | ||
| if (parsed.data && typeof parsed.data === "object" && "message_id" in parsed.data) { | ||
| return event; | ||
| } | ||
| if (parsed.data && typeof parsed.data === "object" && "object" in parsed.data) { | ||
| return event; | ||
| } | ||
| if (!parsed.data) { | ||
| throw new WebhookSignatureError("Invalid event structure"); | ||
| } | ||
| return event; | ||
| } | ||
| /** | ||
| * Generate a webhook signature (static method for backwards compatibility) | ||
| * @param payload - Payload to sign | ||
| * @param secret - Secret to use for signing | ||
| */ | ||
| static generateSignature(payload, secret) { | ||
| return generateWebhookSignature(payload, secret); | ||
| } | ||
| }; | ||
@@ -1104,0 +1642,0 @@ export { |
+1
-1
| { | ||
| "name": "@sendly/node", | ||
| "version": "1.0.8", | ||
| "version": "1.1.0", | ||
| "description": "Official Sendly Node.js SDK for SMS messaging", | ||
@@ -5,0 +5,0 @@ "main": "./dist/index.js", |
204430
54.05%5260
50.98%