@linqapp/sdk 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/api-promise.d.mts +1 -0
  3. package/api-promise.d.mts.map +1 -1
  4. package/api-promise.d.ts +1 -0
  5. package/api-promise.d.ts.map +1 -1
  6. package/api-promise.mjs +1 -0
  7. package/api-promise.mjs.map +1 -1
  8. package/client.d.mts +162 -84
  9. package/client.d.mts.map +1 -1
  10. package/client.d.ts +162 -84
  11. package/client.d.ts.map +1 -1
  12. package/client.js +162 -84
  13. package/client.js.map +1 -1
  14. package/client.mjs +162 -84
  15. package/client.mjs.map +1 -1
  16. package/core/api-promise.d.mts.map +1 -1
  17. package/core/api-promise.d.ts.map +1 -1
  18. package/core/api-promise.mjs.map +1 -1
  19. package/core/error.mjs.map +1 -1
  20. package/core/pagination.d.mts.map +1 -1
  21. package/core/pagination.d.ts.map +1 -1
  22. package/core/pagination.mjs.map +1 -1
  23. package/core/resource.d.mts.map +1 -1
  24. package/core/resource.d.ts.map +1 -1
  25. package/core/uploads.d.mts.map +1 -1
  26. package/core/uploads.d.ts.map +1 -1
  27. package/core/uploads.mjs.map +1 -1
  28. package/error.d.mts +1 -0
  29. package/error.d.mts.map +1 -1
  30. package/error.d.ts +1 -0
  31. package/error.d.ts.map +1 -1
  32. package/error.mjs +1 -0
  33. package/error.mjs.map +1 -1
  34. package/index.d.mts.map +1 -1
  35. package/index.d.ts.map +1 -1
  36. package/index.mjs.map +1 -1
  37. package/internal/detect-platform.mjs.map +1 -1
  38. package/internal/headers.mjs.map +1 -1
  39. package/internal/parse.d.mts.map +1 -1
  40. package/internal/parse.d.ts.map +1 -1
  41. package/internal/parse.mjs.map +1 -1
  42. package/internal/request-options.d.mts.map +1 -1
  43. package/internal/request-options.d.ts.map +1 -1
  44. package/internal/shims.d.mts +6 -0
  45. package/internal/shims.d.mts.map +1 -1
  46. package/internal/shims.d.ts +6 -0
  47. package/internal/shims.d.ts.map +1 -1
  48. package/internal/to-file.d.mts.map +1 -1
  49. package/internal/to-file.d.ts.map +1 -1
  50. package/internal/to-file.mjs.map +1 -1
  51. package/internal/types.d.mts +4 -4
  52. package/internal/types.d.mts.map +1 -1
  53. package/internal/types.d.ts +4 -4
  54. package/internal/types.d.ts.map +1 -1
  55. package/internal/uploads.d.mts.map +1 -1
  56. package/internal/uploads.d.ts.map +1 -1
  57. package/internal/uploads.mjs.map +1 -1
  58. package/internal/utils/base64.mjs.map +1 -1
  59. package/internal/utils/log.d.mts.map +1 -1
  60. package/internal/utils/log.d.ts.map +1 -1
  61. package/internal/utils/log.mjs.map +1 -1
  62. package/internal/utils/path.mjs.map +1 -1
  63. package/internal/utils/query.mjs.map +1 -1
  64. package/internal/utils/values.mjs.map +1 -1
  65. package/internal/utils.d.mts.map +1 -1
  66. package/internal/utils.d.ts.map +1 -1
  67. package/internal/utils.mjs.map +1 -1
  68. package/package.json +1 -1
  69. package/pagination.d.mts +1 -0
  70. package/pagination.d.mts.map +1 -1
  71. package/pagination.d.ts +1 -0
  72. package/pagination.d.ts.map +1 -1
  73. package/pagination.mjs +1 -0
  74. package/pagination.mjs.map +1 -1
  75. package/resource.d.mts +1 -0
  76. package/resource.d.mts.map +1 -1
  77. package/resource.d.ts +1 -0
  78. package/resource.d.ts.map +1 -1
  79. package/resource.mjs +1 -0
  80. package/resource.mjs.map +1 -1
  81. package/resources/attachments.d.mts.map +1 -1
  82. package/resources/attachments.d.ts.map +1 -1
  83. package/resources/attachments.mjs.map +1 -1
  84. package/resources/capability.d.mts.map +1 -1
  85. package/resources/capability.d.ts.map +1 -1
  86. package/resources/capability.mjs.map +1 -1
  87. package/resources/chats/chats.d.mts +4 -0
  88. package/resources/chats/chats.d.mts.map +1 -1
  89. package/resources/chats/chats.d.ts +4 -0
  90. package/resources/chats/chats.d.ts.map +1 -1
  91. package/resources/chats/chats.js +4 -0
  92. package/resources/chats/chats.js.map +1 -1
  93. package/resources/chats/chats.mjs +4 -0
  94. package/resources/chats/chats.mjs.map +1 -1
  95. package/resources/chats/index.d.mts +1 -0
  96. package/resources/chats/index.d.mts.map +1 -1
  97. package/resources/chats/index.d.ts +1 -0
  98. package/resources/chats/index.d.ts.map +1 -1
  99. package/resources/chats/index.js +3 -1
  100. package/resources/chats/index.js.map +1 -1
  101. package/resources/chats/index.mjs +1 -0
  102. package/resources/chats/index.mjs.map +1 -1
  103. package/resources/chats/location.d.mts +107 -0
  104. package/resources/chats/location.d.mts.map +1 -0
  105. package/resources/chats/location.d.ts +107 -0
  106. package/resources/chats/location.d.ts.map +1 -0
  107. package/resources/chats/location.js +68 -0
  108. package/resources/chats/location.js.map +1 -0
  109. package/resources/chats/location.mjs +64 -0
  110. package/resources/chats/location.mjs.map +1 -0
  111. package/resources/chats/messages.d.mts.map +1 -1
  112. package/resources/chats/messages.d.ts.map +1 -1
  113. package/resources/chats/messages.mjs.map +1 -1
  114. package/resources/chats/participants.d.mts.map +1 -1
  115. package/resources/chats/participants.d.ts.map +1 -1
  116. package/resources/chats/participants.mjs.map +1 -1
  117. package/resources/chats/typing.d.mts.map +1 -1
  118. package/resources/chats/typing.d.ts.map +1 -1
  119. package/resources/chats/typing.mjs.map +1 -1
  120. package/resources/chats.d.mts.map +1 -1
  121. package/resources/chats.d.ts.map +1 -1
  122. package/resources/chats.mjs.map +1 -1
  123. package/resources/contact-card.d.mts.map +1 -1
  124. package/resources/contact-card.d.ts.map +1 -1
  125. package/resources/contact-card.mjs.map +1 -1
  126. package/resources/index.d.mts.map +1 -1
  127. package/resources/index.d.ts.map +1 -1
  128. package/resources/index.mjs.map +1 -1
  129. package/resources/messages.d.mts.map +1 -1
  130. package/resources/messages.d.ts.map +1 -1
  131. package/resources/messages.mjs.map +1 -1
  132. package/resources/phone-numbers.d.mts.map +1 -1
  133. package/resources/phone-numbers.d.ts.map +1 -1
  134. package/resources/phone-numbers.mjs.map +1 -1
  135. package/resources/phonenumbers.d.mts.map +1 -1
  136. package/resources/phonenumbers.d.ts.map +1 -1
  137. package/resources/phonenumbers.mjs.map +1 -1
  138. package/resources/webhook-events.d.mts +83 -44
  139. package/resources/webhook-events.d.mts.map +1 -1
  140. package/resources/webhook-events.d.ts +83 -44
  141. package/resources/webhook-events.d.ts.map +1 -1
  142. package/resources/webhook-events.js +81 -42
  143. package/resources/webhook-events.js.map +1 -1
  144. package/resources/webhook-events.mjs +81 -42
  145. package/resources/webhook-events.mjs.map +1 -1
  146. package/resources/webhook-subscriptions.d.mts +89 -44
  147. package/resources/webhook-subscriptions.d.mts.map +1 -1
  148. package/resources/webhook-subscriptions.d.ts +89 -44
  149. package/resources/webhook-subscriptions.d.ts.map +1 -1
  150. package/resources/webhook-subscriptions.js +90 -45
  151. package/resources/webhook-subscriptions.js.map +1 -1
  152. package/resources/webhook-subscriptions.mjs +90 -45
  153. package/resources/webhook-subscriptions.mjs.map +1 -1
  154. package/resources/webhooks.d.mts +4 -4
  155. package/resources/webhooks.d.mts.map +1 -1
  156. package/resources/webhooks.d.ts +4 -4
  157. package/resources/webhooks.d.ts.map +1 -1
  158. package/resources/webhooks.mjs.map +1 -1
  159. package/resources.d.mts.map +1 -1
  160. package/resources.d.ts.map +1 -1
  161. package/resources.mjs.map +1 -1
  162. package/src/client.ts +162 -84
  163. package/src/resources/chats/chats.ts +10 -0
  164. package/src/resources/chats/index.ts +1 -0
  165. package/src/resources/chats/location.ts +138 -0
  166. package/src/resources/webhook-events.ts +85 -44
  167. package/src/resources/webhook-subscriptions.ts +89 -44
  168. package/src/resources/webhooks.ts +6 -4
  169. package/src/version.ts +1 -1
  170. package/uploads.d.mts +1 -0
  171. package/uploads.d.mts.map +1 -1
  172. package/uploads.d.ts +1 -0
  173. package/uploads.d.ts.map +1 -1
  174. package/uploads.mjs +1 -0
  175. package/uploads.mjs.map +1 -1
  176. package/version.d.mts +1 -1
  177. package/version.d.ts +1 -1
  178. package/version.js +1 -1
  179. package/version.mjs +1 -1
@@ -14,55 +14,82 @@ import { RequestOptions } from "../internal/request-options.js";
14
14
  *
15
15
  * ## Webhook Headers
16
16
  *
17
- * Each webhook request includes the following headers:
17
+ * All webhook requests include two sets of headers. **If you have an existing integration
18
+ * using the `X-Webhook-*` headers, nothing changes** — those headers are still sent on
19
+ * every delivery and work exactly as before. The new `webhook-*` headers follow the
20
+ * [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification.
21
+ * You can safely ignore them if your current verification code works and you don't want to use this convention.
22
+ *
23
+ * ### Standard Webhooks Headers (Recommended)
24
+ *
25
+ * Used by [our SDK](https://github.com/linq-team/linq-node) and any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks).
18
26
  *
19
27
  * | Header | Description |
20
28
  * |--------|-------------|
21
- * | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
22
- * | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
23
- * | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
24
- * | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
29
+ * | `webhook-id` | Unique event identifier (use as idempotency key) |
30
+ * | `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent |
31
+ * | `webhook-signature` | Standard Webhooks signature (`v1,{base64}` format) |
25
32
  *
26
- * ## Verifying Webhook Signatures
33
+ * ### Legacy Headers (Deprecated)
34
+ *
35
+ * Still sent on every delivery for backwards compatibility. Existing verification code
36
+ * using these headers continues to work — no changes required.
27
37
  *
28
- * All webhooks are signed using HMAC-SHA256. You should always verify the signature
29
- * to ensure the webhook originated from Linq and hasn't been tampered with.
38
+ * | Header | Description |
39
+ * |--------|-------------|
40
+ * | `X-Webhook-Event` | *(deprecated)* Event type (e.g., `message.sent`) |
41
+ * | `X-Webhook-Subscription-ID` | *(deprecated)* Webhook subscription ID |
42
+ * | `X-Webhook-Timestamp` | *(deprecated)* Unix timestamp (seconds) |
43
+ * | `X-Webhook-Signature` | *(deprecated)* HMAC-SHA256 signature (hex-encoded) |
30
44
  *
31
- * **Signature Construction:**
45
+ * ## Signing Secrets
32
46
  *
33
- * The signature is computed over a concatenation of the timestamp and payload:
47
+ * Signing secrets use the Standard Webhooks format: a `whsec_` prefix followed
48
+ * by base64-encoded random bytes (e.g., `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).
34
49
  *
35
- * ```
36
- * {timestamp}.{payload}
37
- * ```
50
+ * Strip the `whsec_` prefix and base64-decode the remainder to get the raw key bytes.
38
51
  *
39
- * Where:
40
- * - `timestamp` is the value from the `X-Webhook-Timestamp` header
41
- * - `payload` is the raw JSON request body (exact bytes, not re-serialized)
52
+ * ## Verifying Webhook Signatures
53
+ *
54
+ * Webhooks are signed following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks).
55
+ * You can use any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) to verify
56
+ * signatures, or implement verification manually:
57
+ *
58
+ * **Signed content:** `{webhook-id}.{webhook-timestamp}.{body}`
42
59
  *
43
60
  * **Verification Steps:**
44
61
  *
45
- * 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
46
- * 2. Get the raw request body bytes (do not parse and re-serialize)
47
- * 3. Concatenate: `"{timestamp}.{payload}"`
48
- * 4. Compute HMAC-SHA256 using your signing secret as the key
49
- * 5. Hex-encode the result and compare with `X-Webhook-Signature`
50
- * 6. Use constant-time comparison to prevent timing attacks
62
+ * 1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers
63
+ * 2. Reject if the timestamp is more than 5 minutes old (replay protection)
64
+ * 3. Get the raw request body bytes (do not parse and re-serialize)
65
+ * 4. Construct signed content: `"{webhook-id}.{webhook-timestamp}.{body}"`
66
+ * 5. Strip the `whsec_` prefix from your secret and base64-decode to get key bytes
67
+ * 6. Compute HMAC-SHA256 using the key bytes over the signed content
68
+ * 7. Base64-encode the result and compare with the value after `v1,` in `webhook-signature`
69
+ * 8. Use constant-time comparison to prevent timing attacks
51
70
  *
52
71
  * **Example (Python):**
53
72
  *
54
73
  * ```python
55
- * import hmac
56
- * import hashlib
57
- *
58
- * def verify_webhook(signing_secret, payload, timestamp, signature):
59
- * message = f"{timestamp}.{payload.decode('utf-8')}"
60
- * expected = hmac.new(
61
- * signing_secret.encode('utf-8'),
62
- * message.encode('utf-8'),
63
- * hashlib.sha256
64
- * ).hexdigest()
65
- * return hmac.compare_digest(expected, signature)
74
+ * import base64, hmac, hashlib
75
+ *
76
+ * def verify_webhook(secret, body, headers):
77
+ * msg_id = headers['webhook-id']
78
+ * timestamp = headers['webhook-timestamp']
79
+ * signature = headers['webhook-signature']
80
+ *
81
+ * secret_str = secret.removeprefix('whsec_')
82
+ * key = base64.b64decode(secret_str)
83
+ *
84
+ * signed_content = f"{msg_id}.{timestamp}.{body}"
85
+ * expected = base64.b64encode(
86
+ * hmac.new(key, signed_content.encode(), hashlib.sha256).digest()
87
+ * ).decode()
88
+ *
89
+ * for sig in signature.split(' '):
90
+ * if sig.startswith('v1,') and hmac.compare_digest(expected, sig[3:]):
91
+ * return True
92
+ * return False
66
93
  * ```
67
94
  *
68
95
  * **Example (Node.js):**
@@ -70,16 +97,28 @@ import { RequestOptions } from "../internal/request-options.js";
70
97
  * ```javascript
71
98
  * const crypto = require('crypto');
72
99
  *
73
- * function verifyWebhook(signingSecret, payload, timestamp, signature) {
74
- * const message = `${timestamp}.${payload}`;
100
+ * function verifyWebhook(secret, rawBody, headers) {
101
+ * const msgId = headers['webhook-id'];
102
+ * const timestamp = headers['webhook-timestamp'];
103
+ * const signature = headers['webhook-signature'];
104
+ *
105
+ * const secretStr = secret.startsWith('whsec_') ? secret.slice(6) : secret;
106
+ * const keyBytes = Buffer.from(secretStr, 'base64');
107
+ * const signedContent = `${msgId}.${timestamp}.${rawBody}`;
75
108
  * const expected = crypto
76
- * .createHmac('sha256', signingSecret)
77
- * .update(message)
78
- * .digest('hex');
79
- * return crypto.timingSafeEqual(
80
- * Buffer.from(expected),
81
- * Buffer.from(signature)
82
- * );
109
+ * .createHmac('sha256', keyBytes)
110
+ * .update(signedContent)
111
+ * .digest('base64');
112
+ *
113
+ * return signature.split(' ').some(sig => {
114
+ * if (!sig.startsWith('v1,')) return false;
115
+ * try {
116
+ * return crypto.timingSafeEqual(
117
+ * Buffer.from(expected, 'base64'),
118
+ * Buffer.from(sig.slice(3), 'base64')
119
+ * );
120
+ * } catch { return false; }
121
+ * });
83
122
  * }
84
123
  * ```
85
124
  *
@@ -105,12 +144,12 @@ export declare class WebhookEvents extends APIResource {
105
144
  * `webhook_version: "2026-02-03"`. Subscribing to this event on a v2025
106
145
  * subscription will not produce any deliveries.
107
146
  */
108
- export type WebhookEventType = 'message.sent' | 'message.received' | 'message.read' | 'message.delivered' | 'message.failed' | 'message.edited' | 'reaction.added' | 'reaction.removed' | 'participant.added' | 'participant.removed' | 'chat.created' | 'chat.group_name_updated' | 'chat.group_icon_updated' | 'chat.group_name_update_failed' | 'chat.group_icon_update_failed' | 'chat.typing_indicator.started' | 'chat.typing_indicator.stopped' | 'phone_number.status_updated' | 'call.initiated' | 'call.ringing' | 'call.answered' | 'call.ended' | 'call.failed' | 'call.declined' | 'call.no_answer';
147
+ export type WebhookEventType = 'message.sent' | 'message.received' | 'message.read' | 'message.delivered' | 'message.failed' | 'message.edited' | 'reaction.added' | 'reaction.removed' | 'participant.added' | 'participant.removed' | 'chat.created' | 'chat.group_name_updated' | 'chat.group_icon_updated' | 'chat.group_name_update_failed' | 'chat.group_icon_update_failed' | 'chat.typing_indicator.started' | 'chat.typing_indicator.stopped' | 'phone_number.status_updated' | 'call.initiated' | 'call.ringing' | 'call.answered' | 'call.ended' | 'call.failed' | 'call.declined' | 'call.no_answer' | 'location.sharing.started' | 'location.sharing.stopped';
109
148
  export interface WebhookEventListResponse {
110
149
  /**
111
150
  * URL to the webhook events documentation
112
151
  */
113
- doc_url: 'https://apidocs.linqapp.com/documentation/webhook-events';
152
+ doc_url: 'https://docs.linqapp.com/guides/webhooks/events';
114
153
  /**
115
154
  * List of all available webhook event types
116
155
  */
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-events.d.ts","sourceRoot":"","sources":["../src/resources/webhook-events.ts"],"names":[],"mappings":"OAEO,EAAE,WAAW,EAAE;OACf,EAAE,UAAU,EAAE;OACd,EAAE,cAAc,EAAE;AAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AACH,qBAAa,aAAc,SAAQ,WAAW;IAC5C;;;;OAIG;IACH,IAAI,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,UAAU,CAAC,wBAAwB,CAAC;CAGrE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,kBAAkB,GAClB,cAAc,GACd,mBAAmB,GACnB,gBAAgB,GAChB,gBAAgB,GAChB,gBAAgB,GAChB,kBAAkB,GAClB,mBAAmB,GACnB,qBAAqB,GACrB,cAAc,GACd,yBAAyB,GACzB,yBAAyB,GACzB,+BAA+B,GAC/B,+BAA+B,GAC/B,+BAA+B,GAC/B,+BAA+B,GAC/B,6BAA6B,GAC7B,gBAAgB,GAChB,cAAc,GACd,eAAe,GACf,YAAY,GACZ,aAAa,GACb,eAAe,GACf,gBAAgB,CAAC;AAErB,MAAM,WAAW,wBAAwB;IACvC;;OAEG;IACH,OAAO,EAAE,0DAA0D,CAAC;IAEpE;;OAEG;IACH,MAAM,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;CACjC;AAED,MAAM,CAAC,OAAO,WAAW,aAAa,CAAC;IACrC,OAAO,EACL,KAAK,gBAAgB,IAAI,gBAAgB,EACzC,KAAK,wBAAwB,IAAI,wBAAwB,GAC1D,CAAC;CACH"}
1
+ {"version":3,"file":"webhook-events.d.ts","sourceRoot":"","sources":["../src/resources/webhook-events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,4BAAyB;AAC/C,OAAO,EAAE,UAAU,EAAE,+BAA4B;AACjD,OAAO,EAAE,cAAc,EAAE,uCAAoC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+HG;AACH,qBAAa,aAAc,SAAQ,WAAW;IAC5C;;;;OAIG;IACH,IAAI,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,UAAU,CAAC,wBAAwB,CAAC;CAGrE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,kBAAkB,GAClB,cAAc,GACd,mBAAmB,GACnB,gBAAgB,GAChB,gBAAgB,GAChB,gBAAgB,GAChB,kBAAkB,GAClB,mBAAmB,GACnB,qBAAqB,GACrB,cAAc,GACd,yBAAyB,GACzB,yBAAyB,GACzB,+BAA+B,GAC/B,+BAA+B,GAC/B,+BAA+B,GAC/B,+BAA+B,GAC/B,6BAA6B,GAC7B,gBAAgB,GAChB,cAAc,GACd,eAAe,GACf,YAAY,GACZ,aAAa,GACb,eAAe,GACf,gBAAgB,GAChB,0BAA0B,GAC1B,0BAA0B,CAAC;AAE/B,MAAM,WAAW,wBAAwB;IACvC;;OAEG;IACH,OAAO,EAAE,iDAAiD,CAAC;IAE3D;;OAEG;IACH,MAAM,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;CACjC;AAED,MAAM,CAAC,OAAO,WAAW,aAAa,CAAC;IACrC,OAAO,EACL,KAAK,gBAAgB,IAAI,gBAAgB,EACzC,KAAK,wBAAwB,IAAI,wBAAwB,GAC1D,CAAC;CACH"}
@@ -16,55 +16,82 @@ const resource_1 = require("../core/resource.js");
16
16
  *
17
17
  * ## Webhook Headers
18
18
  *
19
- * Each webhook request includes the following headers:
19
+ * All webhook requests include two sets of headers. **If you have an existing integration
20
+ * using the `X-Webhook-*` headers, nothing changes** — those headers are still sent on
21
+ * every delivery and work exactly as before. The new `webhook-*` headers follow the
22
+ * [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification.
23
+ * You can safely ignore them if your current verification code works and you don't want to use this convention.
24
+ *
25
+ * ### Standard Webhooks Headers (Recommended)
26
+ *
27
+ * Used by [our SDK](https://github.com/linq-team/linq-node) and any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks).
20
28
  *
21
29
  * | Header | Description |
22
30
  * |--------|-------------|
23
- * | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
24
- * | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
25
- * | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
26
- * | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
31
+ * | `webhook-id` | Unique event identifier (use as idempotency key) |
32
+ * | `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent |
33
+ * | `webhook-signature` | Standard Webhooks signature (`v1,{base64}` format) |
27
34
  *
28
- * ## Verifying Webhook Signatures
35
+ * ### Legacy Headers (Deprecated)
36
+ *
37
+ * Still sent on every delivery for backwards compatibility. Existing verification code
38
+ * using these headers continues to work — no changes required.
29
39
  *
30
- * All webhooks are signed using HMAC-SHA256. You should always verify the signature
31
- * to ensure the webhook originated from Linq and hasn't been tampered with.
40
+ * | Header | Description |
41
+ * |--------|-------------|
42
+ * | `X-Webhook-Event` | *(deprecated)* Event type (e.g., `message.sent`) |
43
+ * | `X-Webhook-Subscription-ID` | *(deprecated)* Webhook subscription ID |
44
+ * | `X-Webhook-Timestamp` | *(deprecated)* Unix timestamp (seconds) |
45
+ * | `X-Webhook-Signature` | *(deprecated)* HMAC-SHA256 signature (hex-encoded) |
32
46
  *
33
- * **Signature Construction:**
47
+ * ## Signing Secrets
34
48
  *
35
- * The signature is computed over a concatenation of the timestamp and payload:
49
+ * Signing secrets use the Standard Webhooks format: a `whsec_` prefix followed
50
+ * by base64-encoded random bytes (e.g., `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).
36
51
  *
37
- * ```
38
- * {timestamp}.{payload}
39
- * ```
52
+ * Strip the `whsec_` prefix and base64-decode the remainder to get the raw key bytes.
40
53
  *
41
- * Where:
42
- * - `timestamp` is the value from the `X-Webhook-Timestamp` header
43
- * - `payload` is the raw JSON request body (exact bytes, not re-serialized)
54
+ * ## Verifying Webhook Signatures
55
+ *
56
+ * Webhooks are signed following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks).
57
+ * You can use any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) to verify
58
+ * signatures, or implement verification manually:
59
+ *
60
+ * **Signed content:** `{webhook-id}.{webhook-timestamp}.{body}`
44
61
  *
45
62
  * **Verification Steps:**
46
63
  *
47
- * 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
48
- * 2. Get the raw request body bytes (do not parse and re-serialize)
49
- * 3. Concatenate: `"{timestamp}.{payload}"`
50
- * 4. Compute HMAC-SHA256 using your signing secret as the key
51
- * 5. Hex-encode the result and compare with `X-Webhook-Signature`
52
- * 6. Use constant-time comparison to prevent timing attacks
64
+ * 1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers
65
+ * 2. Reject if the timestamp is more than 5 minutes old (replay protection)
66
+ * 3. Get the raw request body bytes (do not parse and re-serialize)
67
+ * 4. Construct signed content: `"{webhook-id}.{webhook-timestamp}.{body}"`
68
+ * 5. Strip the `whsec_` prefix from your secret and base64-decode to get key bytes
69
+ * 6. Compute HMAC-SHA256 using the key bytes over the signed content
70
+ * 7. Base64-encode the result and compare with the value after `v1,` in `webhook-signature`
71
+ * 8. Use constant-time comparison to prevent timing attacks
53
72
  *
54
73
  * **Example (Python):**
55
74
  *
56
75
  * ```python
57
- * import hmac
58
- * import hashlib
59
- *
60
- * def verify_webhook(signing_secret, payload, timestamp, signature):
61
- * message = f"{timestamp}.{payload.decode('utf-8')}"
62
- * expected = hmac.new(
63
- * signing_secret.encode('utf-8'),
64
- * message.encode('utf-8'),
65
- * hashlib.sha256
66
- * ).hexdigest()
67
- * return hmac.compare_digest(expected, signature)
76
+ * import base64, hmac, hashlib
77
+ *
78
+ * def verify_webhook(secret, body, headers):
79
+ * msg_id = headers['webhook-id']
80
+ * timestamp = headers['webhook-timestamp']
81
+ * signature = headers['webhook-signature']
82
+ *
83
+ * secret_str = secret.removeprefix('whsec_')
84
+ * key = base64.b64decode(secret_str)
85
+ *
86
+ * signed_content = f"{msg_id}.{timestamp}.{body}"
87
+ * expected = base64.b64encode(
88
+ * hmac.new(key, signed_content.encode(), hashlib.sha256).digest()
89
+ * ).decode()
90
+ *
91
+ * for sig in signature.split(' '):
92
+ * if sig.startswith('v1,') and hmac.compare_digest(expected, sig[3:]):
93
+ * return True
94
+ * return False
68
95
  * ```
69
96
  *
70
97
  * **Example (Node.js):**
@@ -72,16 +99,28 @@ const resource_1 = require("../core/resource.js");
72
99
  * ```javascript
73
100
  * const crypto = require('crypto');
74
101
  *
75
- * function verifyWebhook(signingSecret, payload, timestamp, signature) {
76
- * const message = `${timestamp}.${payload}`;
102
+ * function verifyWebhook(secret, rawBody, headers) {
103
+ * const msgId = headers['webhook-id'];
104
+ * const timestamp = headers['webhook-timestamp'];
105
+ * const signature = headers['webhook-signature'];
106
+ *
107
+ * const secretStr = secret.startsWith('whsec_') ? secret.slice(6) : secret;
108
+ * const keyBytes = Buffer.from(secretStr, 'base64');
109
+ * const signedContent = `${msgId}.${timestamp}.${rawBody}`;
77
110
  * const expected = crypto
78
- * .createHmac('sha256', signingSecret)
79
- * .update(message)
80
- * .digest('hex');
81
- * return crypto.timingSafeEqual(
82
- * Buffer.from(expected),
83
- * Buffer.from(signature)
84
- * );
111
+ * .createHmac('sha256', keyBytes)
112
+ * .update(signedContent)
113
+ * .digest('base64');
114
+ *
115
+ * return signature.split(' ').some(sig => {
116
+ * if (!sig.startsWith('v1,')) return false;
117
+ * try {
118
+ * return crypto.timingSafeEqual(
119
+ * Buffer.from(expected, 'base64'),
120
+ * Buffer.from(sig.slice(3), 'base64')
121
+ * );
122
+ * } catch { return false; }
123
+ * });
85
124
  * }
86
125
  * ```
87
126
  *
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-events.js","sourceRoot":"","sources":["../src/resources/webhook-events.ts"],"names":[],"mappings":";AAAA,sFAAsF;;;AAEtF,kDAA+C;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AACH,MAAa,aAAc,SAAQ,sBAAW;IAC5C;;;;OAIG;IACH,IAAI,CAAC,OAAwB;QAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;CACF;AATD,sCASC"}
1
+ {"version":3,"file":"webhook-events.js","sourceRoot":"","sources":["../src/resources/webhook-events.ts"],"names":[],"mappings":";AAAA,sFAAsF;;;AAEtF,kDAA+C;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+HG;AACH,MAAa,aAAc,SAAQ,sBAAW;IAC5C;;;;OAIG;IACH,IAAI,CAAC,OAAwB;QAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;CACF;AATD,sCASC"}
@@ -13,55 +13,82 @@ import { APIResource } from "../core/resource.mjs";
13
13
  *
14
14
  * ## Webhook Headers
15
15
  *
16
- * Each webhook request includes the following headers:
16
+ * All webhook requests include two sets of headers. **If you have an existing integration
17
+ * using the `X-Webhook-*` headers, nothing changes** — those headers are still sent on
18
+ * every delivery and work exactly as before. The new `webhook-*` headers follow the
19
+ * [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification.
20
+ * You can safely ignore them if your current verification code works and you don't want to use this convention.
21
+ *
22
+ * ### Standard Webhooks Headers (Recommended)
23
+ *
24
+ * Used by [our SDK](https://github.com/linq-team/linq-node) and any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks).
17
25
  *
18
26
  * | Header | Description |
19
27
  * |--------|-------------|
20
- * | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
21
- * | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
22
- * | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
23
- * | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
28
+ * | `webhook-id` | Unique event identifier (use as idempotency key) |
29
+ * | `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent |
30
+ * | `webhook-signature` | Standard Webhooks signature (`v1,{base64}` format) |
24
31
  *
25
- * ## Verifying Webhook Signatures
32
+ * ### Legacy Headers (Deprecated)
33
+ *
34
+ * Still sent on every delivery for backwards compatibility. Existing verification code
35
+ * using these headers continues to work — no changes required.
26
36
  *
27
- * All webhooks are signed using HMAC-SHA256. You should always verify the signature
28
- * to ensure the webhook originated from Linq and hasn't been tampered with.
37
+ * | Header | Description |
38
+ * |--------|-------------|
39
+ * | `X-Webhook-Event` | *(deprecated)* Event type (e.g., `message.sent`) |
40
+ * | `X-Webhook-Subscription-ID` | *(deprecated)* Webhook subscription ID |
41
+ * | `X-Webhook-Timestamp` | *(deprecated)* Unix timestamp (seconds) |
42
+ * | `X-Webhook-Signature` | *(deprecated)* HMAC-SHA256 signature (hex-encoded) |
29
43
  *
30
- * **Signature Construction:**
44
+ * ## Signing Secrets
31
45
  *
32
- * The signature is computed over a concatenation of the timestamp and payload:
46
+ * Signing secrets use the Standard Webhooks format: a `whsec_` prefix followed
47
+ * by base64-encoded random bytes (e.g., `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).
33
48
  *
34
- * ```
35
- * {timestamp}.{payload}
36
- * ```
49
+ * Strip the `whsec_` prefix and base64-decode the remainder to get the raw key bytes.
37
50
  *
38
- * Where:
39
- * - `timestamp` is the value from the `X-Webhook-Timestamp` header
40
- * - `payload` is the raw JSON request body (exact bytes, not re-serialized)
51
+ * ## Verifying Webhook Signatures
52
+ *
53
+ * Webhooks are signed following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks).
54
+ * You can use any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) to verify
55
+ * signatures, or implement verification manually:
56
+ *
57
+ * **Signed content:** `{webhook-id}.{webhook-timestamp}.{body}`
41
58
  *
42
59
  * **Verification Steps:**
43
60
  *
44
- * 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
45
- * 2. Get the raw request body bytes (do not parse and re-serialize)
46
- * 3. Concatenate: `"{timestamp}.{payload}"`
47
- * 4. Compute HMAC-SHA256 using your signing secret as the key
48
- * 5. Hex-encode the result and compare with `X-Webhook-Signature`
49
- * 6. Use constant-time comparison to prevent timing attacks
61
+ * 1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers
62
+ * 2. Reject if the timestamp is more than 5 minutes old (replay protection)
63
+ * 3. Get the raw request body bytes (do not parse and re-serialize)
64
+ * 4. Construct signed content: `"{webhook-id}.{webhook-timestamp}.{body}"`
65
+ * 5. Strip the `whsec_` prefix from your secret and base64-decode to get key bytes
66
+ * 6. Compute HMAC-SHA256 using the key bytes over the signed content
67
+ * 7. Base64-encode the result and compare with the value after `v1,` in `webhook-signature`
68
+ * 8. Use constant-time comparison to prevent timing attacks
50
69
  *
51
70
  * **Example (Python):**
52
71
  *
53
72
  * ```python
54
- * import hmac
55
- * import hashlib
56
- *
57
- * def verify_webhook(signing_secret, payload, timestamp, signature):
58
- * message = f"{timestamp}.{payload.decode('utf-8')}"
59
- * expected = hmac.new(
60
- * signing_secret.encode('utf-8'),
61
- * message.encode('utf-8'),
62
- * hashlib.sha256
63
- * ).hexdigest()
64
- * return hmac.compare_digest(expected, signature)
73
+ * import base64, hmac, hashlib
74
+ *
75
+ * def verify_webhook(secret, body, headers):
76
+ * msg_id = headers['webhook-id']
77
+ * timestamp = headers['webhook-timestamp']
78
+ * signature = headers['webhook-signature']
79
+ *
80
+ * secret_str = secret.removeprefix('whsec_')
81
+ * key = base64.b64decode(secret_str)
82
+ *
83
+ * signed_content = f"{msg_id}.{timestamp}.{body}"
84
+ * expected = base64.b64encode(
85
+ * hmac.new(key, signed_content.encode(), hashlib.sha256).digest()
86
+ * ).decode()
87
+ *
88
+ * for sig in signature.split(' '):
89
+ * if sig.startswith('v1,') and hmac.compare_digest(expected, sig[3:]):
90
+ * return True
91
+ * return False
65
92
  * ```
66
93
  *
67
94
  * **Example (Node.js):**
@@ -69,16 +96,28 @@ import { APIResource } from "../core/resource.mjs";
69
96
  * ```javascript
70
97
  * const crypto = require('crypto');
71
98
  *
72
- * function verifyWebhook(signingSecret, payload, timestamp, signature) {
73
- * const message = `${timestamp}.${payload}`;
99
+ * function verifyWebhook(secret, rawBody, headers) {
100
+ * const msgId = headers['webhook-id'];
101
+ * const timestamp = headers['webhook-timestamp'];
102
+ * const signature = headers['webhook-signature'];
103
+ *
104
+ * const secretStr = secret.startsWith('whsec_') ? secret.slice(6) : secret;
105
+ * const keyBytes = Buffer.from(secretStr, 'base64');
106
+ * const signedContent = `${msgId}.${timestamp}.${rawBody}`;
74
107
  * const expected = crypto
75
- * .createHmac('sha256', signingSecret)
76
- * .update(message)
77
- * .digest('hex');
78
- * return crypto.timingSafeEqual(
79
- * Buffer.from(expected),
80
- * Buffer.from(signature)
81
- * );
108
+ * .createHmac('sha256', keyBytes)
109
+ * .update(signedContent)
110
+ * .digest('base64');
111
+ *
112
+ * return signature.split(' ').some(sig => {
113
+ * if (!sig.startsWith('v1,')) return false;
114
+ * try {
115
+ * return crypto.timingSafeEqual(
116
+ * Buffer.from(expected, 'base64'),
117
+ * Buffer.from(sig.slice(3), 'base64')
118
+ * );
119
+ * } catch { return false; }
120
+ * });
82
121
  * }
83
122
  * ```
84
123
  *
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-events.mjs","sourceRoot":"","sources":["../src/resources/webhook-events.ts"],"names":[],"mappings":"AAAA,sFAAsF;OAE/E,EAAE,WAAW,EAAE;AAItB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AACH,MAAM,OAAO,aAAc,SAAQ,WAAW;IAC5C;;;;OAIG;IACH,IAAI,CAAC,OAAwB;QAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;CACF"}
1
+ {"version":3,"file":"webhook-events.mjs","sourceRoot":"","sources":["../src/resources/webhook-events.ts"],"names":[],"mappings":"AAAA,sFAAsF;AAEtF,OAAO,EAAE,WAAW,EAAE,6BAAyB;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+HG;AACH,MAAM,OAAO,aAAc,SAAQ,WAAW;IAC5C;;;;OAIG;IACH,IAAI,CAAC,OAAwB;QAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;CACF"}