@linqapp/sdk 0.25.0 → 0.27.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 (192) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +2 -2
  3. package/api-promise.d.mts +1 -0
  4. package/api-promise.d.mts.map +1 -1
  5. package/api-promise.d.ts +1 -0
  6. package/api-promise.d.ts.map +1 -1
  7. package/api-promise.mjs +1 -0
  8. package/api-promise.mjs.map +1 -1
  9. package/client.d.mts +176 -87
  10. package/client.d.mts.map +1 -1
  11. package/client.d.ts +176 -87
  12. package/client.d.ts.map +1 -1
  13. package/client.js +167 -85
  14. package/client.js.map +1 -1
  15. package/client.mjs +167 -85
  16. package/client.mjs.map +1 -1
  17. package/core/api-promise.d.mts.map +1 -1
  18. package/core/api-promise.d.ts.map +1 -1
  19. package/core/api-promise.mjs.map +1 -1
  20. package/core/error.mjs.map +1 -1
  21. package/core/pagination.d.mts.map +1 -1
  22. package/core/pagination.d.ts.map +1 -1
  23. package/core/pagination.mjs.map +1 -1
  24. package/core/resource.d.mts.map +1 -1
  25. package/core/resource.d.ts.map +1 -1
  26. package/core/uploads.d.mts.map +1 -1
  27. package/core/uploads.d.ts.map +1 -1
  28. package/core/uploads.mjs.map +1 -1
  29. package/error.d.mts +1 -0
  30. package/error.d.mts.map +1 -1
  31. package/error.d.ts +1 -0
  32. package/error.d.ts.map +1 -1
  33. package/error.mjs +1 -0
  34. package/error.mjs.map +1 -1
  35. package/index.d.mts.map +1 -1
  36. package/index.d.ts.map +1 -1
  37. package/index.mjs.map +1 -1
  38. package/internal/detect-platform.mjs.map +1 -1
  39. package/internal/headers.mjs.map +1 -1
  40. package/internal/parse.d.mts.map +1 -1
  41. package/internal/parse.d.ts.map +1 -1
  42. package/internal/parse.mjs.map +1 -1
  43. package/internal/request-options.d.mts.map +1 -1
  44. package/internal/request-options.d.ts.map +1 -1
  45. package/internal/shims.d.mts +6 -0
  46. package/internal/shims.d.mts.map +1 -1
  47. package/internal/shims.d.ts +6 -0
  48. package/internal/shims.d.ts.map +1 -1
  49. package/internal/to-file.d.mts.map +1 -1
  50. package/internal/to-file.d.ts.map +1 -1
  51. package/internal/to-file.mjs.map +1 -1
  52. package/internal/types.d.mts +4 -4
  53. package/internal/types.d.mts.map +1 -1
  54. package/internal/types.d.ts +4 -4
  55. package/internal/types.d.ts.map +1 -1
  56. package/internal/uploads.d.mts.map +1 -1
  57. package/internal/uploads.d.ts.map +1 -1
  58. package/internal/uploads.mjs.map +1 -1
  59. package/internal/utils/base64.mjs.map +1 -1
  60. package/internal/utils/log.d.mts.map +1 -1
  61. package/internal/utils/log.d.ts.map +1 -1
  62. package/internal/utils/log.mjs.map +1 -1
  63. package/internal/utils/path.mjs.map +1 -1
  64. package/internal/utils/query.mjs.map +1 -1
  65. package/internal/utils/values.mjs.map +1 -1
  66. package/internal/utils.d.mts.map +1 -1
  67. package/internal/utils.d.ts.map +1 -1
  68. package/internal/utils.mjs.map +1 -1
  69. package/package.json +4 -2
  70. package/pagination.d.mts +1 -0
  71. package/pagination.d.mts.map +1 -1
  72. package/pagination.d.ts +1 -0
  73. package/pagination.d.ts.map +1 -1
  74. package/pagination.mjs +1 -0
  75. package/pagination.mjs.map +1 -1
  76. package/resource.d.mts +1 -0
  77. package/resource.d.mts.map +1 -1
  78. package/resource.d.ts +1 -0
  79. package/resource.d.ts.map +1 -1
  80. package/resource.mjs +1 -0
  81. package/resource.mjs.map +1 -1
  82. package/resources/attachments.d.mts.map +1 -1
  83. package/resources/attachments.d.ts.map +1 -1
  84. package/resources/attachments.mjs.map +1 -1
  85. package/resources/capability.d.mts.map +1 -1
  86. package/resources/capability.d.ts.map +1 -1
  87. package/resources/capability.mjs.map +1 -1
  88. package/resources/chats/chats.d.mts +4 -0
  89. package/resources/chats/chats.d.mts.map +1 -1
  90. package/resources/chats/chats.d.ts +4 -0
  91. package/resources/chats/chats.d.ts.map +1 -1
  92. package/resources/chats/chats.js +4 -0
  93. package/resources/chats/chats.js.map +1 -1
  94. package/resources/chats/chats.mjs +4 -0
  95. package/resources/chats/chats.mjs.map +1 -1
  96. package/resources/chats/index.d.mts +1 -0
  97. package/resources/chats/index.d.mts.map +1 -1
  98. package/resources/chats/index.d.ts +1 -0
  99. package/resources/chats/index.d.ts.map +1 -1
  100. package/resources/chats/index.js +3 -1
  101. package/resources/chats/index.js.map +1 -1
  102. package/resources/chats/index.mjs +1 -0
  103. package/resources/chats/index.mjs.map +1 -1
  104. package/resources/chats/location.d.mts +107 -0
  105. package/resources/chats/location.d.mts.map +1 -0
  106. package/resources/chats/location.d.ts +107 -0
  107. package/resources/chats/location.d.ts.map +1 -0
  108. package/resources/chats/location.js +68 -0
  109. package/resources/chats/location.js.map +1 -0
  110. package/resources/chats/location.mjs +64 -0
  111. package/resources/chats/location.mjs.map +1 -0
  112. package/resources/chats/messages.d.mts +3 -2
  113. package/resources/chats/messages.d.mts.map +1 -1
  114. package/resources/chats/messages.d.ts +3 -2
  115. package/resources/chats/messages.d.ts.map +1 -1
  116. package/resources/chats/messages.mjs.map +1 -1
  117. package/resources/chats/participants.d.mts.map +1 -1
  118. package/resources/chats/participants.d.ts.map +1 -1
  119. package/resources/chats/participants.mjs.map +1 -1
  120. package/resources/chats/typing.d.mts.map +1 -1
  121. package/resources/chats/typing.d.ts.map +1 -1
  122. package/resources/chats/typing.mjs.map +1 -1
  123. package/resources/chats.d.mts.map +1 -1
  124. package/resources/chats.d.ts.map +1 -1
  125. package/resources/chats.mjs.map +1 -1
  126. package/resources/contact-card.d.mts.map +1 -1
  127. package/resources/contact-card.d.ts.map +1 -1
  128. package/resources/contact-card.mjs.map +1 -1
  129. package/resources/index.d.mts +1 -1
  130. package/resources/index.d.mts.map +1 -1
  131. package/resources/index.d.ts +1 -1
  132. package/resources/index.d.ts.map +1 -1
  133. package/resources/index.mjs.map +1 -1
  134. package/resources/messages.d.mts +9 -2
  135. package/resources/messages.d.mts.map +1 -1
  136. package/resources/messages.d.ts +9 -2
  137. package/resources/messages.d.ts.map +1 -1
  138. package/resources/messages.mjs.map +1 -1
  139. package/resources/phone-numbers.d.mts.map +1 -1
  140. package/resources/phone-numbers.d.ts.map +1 -1
  141. package/resources/phone-numbers.mjs.map +1 -1
  142. package/resources/phonenumbers.d.mts.map +1 -1
  143. package/resources/phonenumbers.d.ts.map +1 -1
  144. package/resources/phonenumbers.mjs.map +1 -1
  145. package/resources/webhook-events.d.mts +83 -44
  146. package/resources/webhook-events.d.mts.map +1 -1
  147. package/resources/webhook-events.d.ts +83 -44
  148. package/resources/webhook-events.d.ts.map +1 -1
  149. package/resources/webhook-events.js +81 -42
  150. package/resources/webhook-events.js.map +1 -1
  151. package/resources/webhook-events.mjs +81 -42
  152. package/resources/webhook-events.mjs.map +1 -1
  153. package/resources/webhook-subscriptions.d.mts +89 -44
  154. package/resources/webhook-subscriptions.d.mts.map +1 -1
  155. package/resources/webhook-subscriptions.d.ts +89 -44
  156. package/resources/webhook-subscriptions.d.ts.map +1 -1
  157. package/resources/webhook-subscriptions.js +90 -45
  158. package/resources/webhook-subscriptions.js.map +1 -1
  159. package/resources/webhook-subscriptions.mjs +90 -45
  160. package/resources/webhook-subscriptions.mjs.map +1 -1
  161. package/resources/webhooks.d.mts +10 -7
  162. package/resources/webhooks.d.mts.map +1 -1
  163. package/resources/webhooks.d.ts +10 -7
  164. package/resources/webhooks.d.ts.map +1 -1
  165. package/resources/webhooks.js +9 -1
  166. package/resources/webhooks.js.map +1 -1
  167. package/resources/webhooks.mjs +9 -1
  168. package/resources/webhooks.mjs.map +1 -1
  169. package/resources.d.mts.map +1 -1
  170. package/resources.d.ts.map +1 -1
  171. package/resources.mjs.map +1 -1
  172. package/src/client.ts +180 -86
  173. package/src/resources/chats/chats.ts +10 -0
  174. package/src/resources/chats/index.ts +1 -0
  175. package/src/resources/chats/location.ts +138 -0
  176. package/src/resources/chats/messages.ts +3 -2
  177. package/src/resources/index.ts +1 -1
  178. package/src/resources/messages.ts +10 -2
  179. package/src/resources/webhook-events.ts +85 -44
  180. package/src/resources/webhook-subscriptions.ts +89 -44
  181. package/src/resources/webhooks.ts +20 -8
  182. package/src/version.ts +1 -1
  183. package/uploads.d.mts +1 -0
  184. package/uploads.d.mts.map +1 -1
  185. package/uploads.d.ts +1 -0
  186. package/uploads.d.ts.map +1 -1
  187. package/uploads.mjs +1 -0
  188. package/uploads.mjs.map +1 -1
  189. package/version.d.mts +1 -1
  190. package/version.d.ts +1 -1
  191. package/version.js +1 -1
  192. package/version.mjs +1 -1
package/src/client.ts CHANGED
@@ -77,7 +77,6 @@ import {
77
77
  ChatGroupNameUpdatedWebhookEvent,
78
78
  ChatTypingIndicatorStartedWebhookEvent,
79
79
  ChatTypingIndicatorStoppedWebhookEvent,
80
- EventsWebhookEvent,
81
80
  MessageDeliveredWebhookEvent,
82
81
  MessageEditedWebhookEvent,
83
82
  MessageEventV2,
@@ -95,6 +94,7 @@ import {
95
94
  SchemasMediaPartResponse,
96
95
  SchemasMessageEffect,
97
96
  SchemasTextPartResponse,
97
+ UnwrapWebhookEvent,
98
98
  Webhooks,
99
99
  } from './resources/webhooks';
100
100
  import {
@@ -136,6 +136,16 @@ export interface ClientOptions {
136
136
  */
137
137
  apiKey?: string | undefined;
138
138
 
139
+ /**
140
+ * Webhook signing secret used by `client.webhooks.unwrap()` to verify the
141
+ * Standard Webhooks signature on incoming webhook requests.
142
+ *
143
+ * Format: a base64-encoded key, optionally with a `whsec_` prefix
144
+ * (e.g. `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).
145
+ *
146
+ */
147
+ webhookSecret?: string | null | undefined;
148
+
139
149
  /**
140
150
  * Override the default base URL for the API, e.g., "https://api.example.com/v2/"
141
151
  *
@@ -210,6 +220,7 @@ export interface ClientOptions {
210
220
  */
211
221
  export class LinqAPIV3 {
212
222
  apiKey: string;
223
+ webhookSecret: string | null;
213
224
 
214
225
  baseURL: string;
215
226
  maxRetries: number;
@@ -227,6 +238,7 @@ export class LinqAPIV3 {
227
238
  * API Client for interfacing with the Linq API V3 API.
228
239
  *
229
240
  * @param {string | undefined} [opts.apiKey=process.env['LINQ_API_V3_API_KEY'] ?? undefined]
241
+ * @param {string | null | undefined} [opts.webhookSecret=process.env['LINQ_WEBHOOK_SECRET'] ?? null]
230
242
  * @param {string} [opts.baseURL=process.env['LINQ_API_V3_BASE_URL'] ?? https://api.linqapp.com/api/partner] - Override the default base URL for the API.
231
243
  * @param {number} [opts.timeout=1 minute] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
232
244
  * @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls.
@@ -238,6 +250,7 @@ export class LinqAPIV3 {
238
250
  constructor({
239
251
  baseURL = readEnv('LINQ_API_V3_BASE_URL'),
240
252
  apiKey = readEnv('LINQ_API_V3_API_KEY'),
253
+ webhookSecret = readEnv('LINQ_WEBHOOK_SECRET') ?? null,
241
254
  ...opts
242
255
  }: ClientOptions = {}) {
243
256
  if (apiKey === undefined) {
@@ -248,6 +261,7 @@ export class LinqAPIV3 {
248
261
 
249
262
  const options: ClientOptions = {
250
263
  apiKey,
264
+ webhookSecret,
251
265
  ...opts,
252
266
  baseURL: baseURL || `https://api.linqapp.com/api/partner`,
253
267
  };
@@ -282,6 +296,7 @@ export class LinqAPIV3 {
282
296
  this._options = options;
283
297
 
284
298
  this.apiKey = apiKey;
299
+ this.webhookSecret = webhookSecret;
285
300
  }
286
301
 
287
302
  /**
@@ -298,6 +313,7 @@ export class LinqAPIV3 {
298
313
  fetch: this.fetch,
299
314
  fetchOptions: this.fetchOptions,
300
315
  apiKey: this.apiKey,
316
+ webhookSecret: this.webhookSecret,
301
317
  ...options,
302
318
  });
303
319
  return client;
@@ -1093,55 +1109,82 @@ export class LinqAPIV3 {
1093
1109
  *
1094
1110
  * ## Webhook Headers
1095
1111
  *
1096
- * Each webhook request includes the following headers:
1112
+ * All webhook requests include two sets of headers. **If you have an existing integration
1113
+ * using the `X-Webhook-*` headers, nothing changes** — those headers are still sent on
1114
+ * every delivery and work exactly as before. The new `webhook-*` headers follow the
1115
+ * [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification.
1116
+ * You can safely ignore them if your current verification code works and you don't want to use this convention.
1117
+ *
1118
+ * ### Standard Webhooks Headers (Recommended)
1119
+ *
1120
+ * Used by [our SDK](https://github.com/linq-team/linq-node) and any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks).
1097
1121
  *
1098
1122
  * | Header | Description |
1099
1123
  * |--------|-------------|
1100
- * | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
1101
- * | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
1102
- * | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
1103
- * | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
1124
+ * | `webhook-id` | Unique event identifier (use as idempotency key) |
1125
+ * | `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent |
1126
+ * | `webhook-signature` | Standard Webhooks signature (`v1,{base64}` format) |
1104
1127
  *
1105
- * ## Verifying Webhook Signatures
1128
+ * ### Legacy Headers (Deprecated)
1106
1129
  *
1107
- * All webhooks are signed using HMAC-SHA256. You should always verify the signature
1108
- * to ensure the webhook originated from Linq and hasn't been tampered with.
1130
+ * Still sent on every delivery for backwards compatibility. Existing verification code
1131
+ * using these headers continues to work — no changes required.
1109
1132
  *
1110
- * **Signature Construction:**
1133
+ * | Header | Description |
1134
+ * |--------|-------------|
1135
+ * | `X-Webhook-Event` | *(deprecated)* Event type (e.g., `message.sent`) |
1136
+ * | `X-Webhook-Subscription-ID` | *(deprecated)* Webhook subscription ID |
1137
+ * | `X-Webhook-Timestamp` | *(deprecated)* Unix timestamp (seconds) |
1138
+ * | `X-Webhook-Signature` | *(deprecated)* HMAC-SHA256 signature (hex-encoded) |
1111
1139
  *
1112
- * The signature is computed over a concatenation of the timestamp and payload:
1140
+ * ## Signing Secrets
1113
1141
  *
1114
- * ```
1115
- * {timestamp}.{payload}
1116
- * ```
1142
+ * Signing secrets use the Standard Webhooks format: a `whsec_` prefix followed
1143
+ * by base64-encoded random bytes (e.g., `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).
1144
+ *
1145
+ * Strip the `whsec_` prefix and base64-decode the remainder to get the raw key bytes.
1146
+ *
1147
+ * ## Verifying Webhook Signatures
1117
1148
  *
1118
- * Where:
1119
- * - `timestamp` is the value from the `X-Webhook-Timestamp` header
1120
- * - `payload` is the raw JSON request body (exact bytes, not re-serialized)
1149
+ * Webhooks are signed following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks).
1150
+ * You can use any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) to verify
1151
+ * signatures, or implement verification manually:
1152
+ *
1153
+ * **Signed content:** `{webhook-id}.{webhook-timestamp}.{body}`
1121
1154
  *
1122
1155
  * **Verification Steps:**
1123
1156
  *
1124
- * 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
1125
- * 2. Get the raw request body bytes (do not parse and re-serialize)
1126
- * 3. Concatenate: `"{timestamp}.{payload}"`
1127
- * 4. Compute HMAC-SHA256 using your signing secret as the key
1128
- * 5. Hex-encode the result and compare with `X-Webhook-Signature`
1129
- * 6. Use constant-time comparison to prevent timing attacks
1157
+ * 1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers
1158
+ * 2. Reject if the timestamp is more than 5 minutes old (replay protection)
1159
+ * 3. Get the raw request body bytes (do not parse and re-serialize)
1160
+ * 4. Construct signed content: `"{webhook-id}.{webhook-timestamp}.{body}"`
1161
+ * 5. Strip the `whsec_` prefix from your secret and base64-decode to get key bytes
1162
+ * 6. Compute HMAC-SHA256 using the key bytes over the signed content
1163
+ * 7. Base64-encode the result and compare with the value after `v1,` in `webhook-signature`
1164
+ * 8. Use constant-time comparison to prevent timing attacks
1130
1165
  *
1131
1166
  * **Example (Python):**
1132
1167
  *
1133
1168
  * ```python
1134
- * import hmac
1135
- * import hashlib
1136
- *
1137
- * def verify_webhook(signing_secret, payload, timestamp, signature):
1138
- * message = f"{timestamp}.{payload.decode('utf-8')}"
1139
- * expected = hmac.new(
1140
- * signing_secret.encode('utf-8'),
1141
- * message.encode('utf-8'),
1142
- * hashlib.sha256
1143
- * ).hexdigest()
1144
- * return hmac.compare_digest(expected, signature)
1169
+ * import base64, hmac, hashlib
1170
+ *
1171
+ * def verify_webhook(secret, body, headers):
1172
+ * msg_id = headers['webhook-id']
1173
+ * timestamp = headers['webhook-timestamp']
1174
+ * signature = headers['webhook-signature']
1175
+ *
1176
+ * secret_str = secret.removeprefix('whsec_')
1177
+ * key = base64.b64decode(secret_str)
1178
+ *
1179
+ * signed_content = f"{msg_id}.{timestamp}.{body}"
1180
+ * expected = base64.b64encode(
1181
+ * hmac.new(key, signed_content.encode(), hashlib.sha256).digest()
1182
+ * ).decode()
1183
+ *
1184
+ * for sig in signature.split(' '):
1185
+ * if sig.startswith('v1,') and hmac.compare_digest(expected, sig[3:]):
1186
+ * return True
1187
+ * return False
1145
1188
  * ```
1146
1189
  *
1147
1190
  * **Example (Node.js):**
@@ -1149,16 +1192,28 @@ export class LinqAPIV3 {
1149
1192
  * ```javascript
1150
1193
  * const crypto = require('crypto');
1151
1194
  *
1152
- * function verifyWebhook(signingSecret, payload, timestamp, signature) {
1153
- * const message = `${timestamp}.${payload}`;
1195
+ * function verifyWebhook(secret, rawBody, headers) {
1196
+ * const msgId = headers['webhook-id'];
1197
+ * const timestamp = headers['webhook-timestamp'];
1198
+ * const signature = headers['webhook-signature'];
1199
+ *
1200
+ * const secretStr = secret.startsWith('whsec_') ? secret.slice(6) : secret;
1201
+ * const keyBytes = Buffer.from(secretStr, 'base64');
1202
+ * const signedContent = `${msgId}.${timestamp}.${rawBody}`;
1154
1203
  * const expected = crypto
1155
- * .createHmac('sha256', signingSecret)
1156
- * .update(message)
1157
- * .digest('hex');
1158
- * return crypto.timingSafeEqual(
1159
- * Buffer.from(expected),
1160
- * Buffer.from(signature)
1161
- * );
1204
+ * .createHmac('sha256', keyBytes)
1205
+ * .update(signedContent)
1206
+ * .digest('base64');
1207
+ *
1208
+ * return signature.split(' ').some(sig => {
1209
+ * if (!sig.startsWith('v1,')) return false;
1210
+ * try {
1211
+ * return crypto.timingSafeEqual(
1212
+ * Buffer.from(expected, 'base64'),
1213
+ * Buffer.from(sig.slice(3), 'base64')
1214
+ * );
1215
+ * } catch { return false; }
1216
+ * });
1162
1217
  * }
1163
1218
  * ```
1164
1219
  *
@@ -1184,55 +1239,82 @@ export class LinqAPIV3 {
1184
1239
  *
1185
1240
  * ## Webhook Headers
1186
1241
  *
1187
- * Each webhook request includes the following headers:
1242
+ * All webhook requests include two sets of headers. **If you have an existing integration
1243
+ * using the `X-Webhook-*` headers, nothing changes** — those headers are still sent on
1244
+ * every delivery and work exactly as before. The new `webhook-*` headers follow the
1245
+ * [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification.
1246
+ * You can safely ignore them if your current verification code works and you don't want to use this convention.
1247
+ *
1248
+ * ### Standard Webhooks Headers (Recommended)
1249
+ *
1250
+ * Used by [our SDK](https://github.com/linq-team/linq-node) and any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks).
1188
1251
  *
1189
1252
  * | Header | Description |
1190
1253
  * |--------|-------------|
1191
- * | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
1192
- * | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
1193
- * | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
1194
- * | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
1254
+ * | `webhook-id` | Unique event identifier (use as idempotency key) |
1255
+ * | `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent |
1256
+ * | `webhook-signature` | Standard Webhooks signature (`v1,{base64}` format) |
1195
1257
  *
1196
- * ## Verifying Webhook Signatures
1258
+ * ### Legacy Headers (Deprecated)
1197
1259
  *
1198
- * All webhooks are signed using HMAC-SHA256. You should always verify the signature
1199
- * to ensure the webhook originated from Linq and hasn't been tampered with.
1260
+ * Still sent on every delivery for backwards compatibility. Existing verification code
1261
+ * using these headers continues to work — no changes required.
1200
1262
  *
1201
- * **Signature Construction:**
1263
+ * | Header | Description |
1264
+ * |--------|-------------|
1265
+ * | `X-Webhook-Event` | *(deprecated)* Event type (e.g., `message.sent`) |
1266
+ * | `X-Webhook-Subscription-ID` | *(deprecated)* Webhook subscription ID |
1267
+ * | `X-Webhook-Timestamp` | *(deprecated)* Unix timestamp (seconds) |
1268
+ * | `X-Webhook-Signature` | *(deprecated)* HMAC-SHA256 signature (hex-encoded) |
1202
1269
  *
1203
- * The signature is computed over a concatenation of the timestamp and payload:
1270
+ * ## Signing Secrets
1204
1271
  *
1205
- * ```
1206
- * {timestamp}.{payload}
1207
- * ```
1272
+ * Signing secrets use the Standard Webhooks format: a `whsec_` prefix followed
1273
+ * by base64-encoded random bytes (e.g., `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).
1208
1274
  *
1209
- * Where:
1210
- * - `timestamp` is the value from the `X-Webhook-Timestamp` header
1211
- * - `payload` is the raw JSON request body (exact bytes, not re-serialized)
1275
+ * Strip the `whsec_` prefix and base64-decode the remainder to get the raw key bytes.
1276
+ *
1277
+ * ## Verifying Webhook Signatures
1278
+ *
1279
+ * Webhooks are signed following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks).
1280
+ * You can use any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) to verify
1281
+ * signatures, or implement verification manually:
1282
+ *
1283
+ * **Signed content:** `{webhook-id}.{webhook-timestamp}.{body}`
1212
1284
  *
1213
1285
  * **Verification Steps:**
1214
1286
  *
1215
- * 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
1216
- * 2. Get the raw request body bytes (do not parse and re-serialize)
1217
- * 3. Concatenate: `"{timestamp}.{payload}"`
1218
- * 4. Compute HMAC-SHA256 using your signing secret as the key
1219
- * 5. Hex-encode the result and compare with `X-Webhook-Signature`
1220
- * 6. Use constant-time comparison to prevent timing attacks
1287
+ * 1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers
1288
+ * 2. Reject if the timestamp is more than 5 minutes old (replay protection)
1289
+ * 3. Get the raw request body bytes (do not parse and re-serialize)
1290
+ * 4. Construct signed content: `"{webhook-id}.{webhook-timestamp}.{body}"`
1291
+ * 5. Strip the `whsec_` prefix from your secret and base64-decode to get key bytes
1292
+ * 6. Compute HMAC-SHA256 using the key bytes over the signed content
1293
+ * 7. Base64-encode the result and compare with the value after `v1,` in `webhook-signature`
1294
+ * 8. Use constant-time comparison to prevent timing attacks
1221
1295
  *
1222
1296
  * **Example (Python):**
1223
1297
  *
1224
1298
  * ```python
1225
- * import hmac
1226
- * import hashlib
1227
- *
1228
- * def verify_webhook(signing_secret, payload, timestamp, signature):
1229
- * message = f"{timestamp}.{payload.decode('utf-8')}"
1230
- * expected = hmac.new(
1231
- * signing_secret.encode('utf-8'),
1232
- * message.encode('utf-8'),
1233
- * hashlib.sha256
1234
- * ).hexdigest()
1235
- * return hmac.compare_digest(expected, signature)
1299
+ * import base64, hmac, hashlib
1300
+ *
1301
+ * def verify_webhook(secret, body, headers):
1302
+ * msg_id = headers['webhook-id']
1303
+ * timestamp = headers['webhook-timestamp']
1304
+ * signature = headers['webhook-signature']
1305
+ *
1306
+ * secret_str = secret.removeprefix('whsec_')
1307
+ * key = base64.b64decode(secret_str)
1308
+ *
1309
+ * signed_content = f"{msg_id}.{timestamp}.{body}"
1310
+ * expected = base64.b64encode(
1311
+ * hmac.new(key, signed_content.encode(), hashlib.sha256).digest()
1312
+ * ).decode()
1313
+ *
1314
+ * for sig in signature.split(' '):
1315
+ * if sig.startswith('v1,') and hmac.compare_digest(expected, sig[3:]):
1316
+ * return True
1317
+ * return False
1236
1318
  * ```
1237
1319
  *
1238
1320
  * **Example (Node.js):**
@@ -1240,16 +1322,28 @@ export class LinqAPIV3 {
1240
1322
  * ```javascript
1241
1323
  * const crypto = require('crypto');
1242
1324
  *
1243
- * function verifyWebhook(signingSecret, payload, timestamp, signature) {
1244
- * const message = `${timestamp}.${payload}`;
1325
+ * function verifyWebhook(secret, rawBody, headers) {
1326
+ * const msgId = headers['webhook-id'];
1327
+ * const timestamp = headers['webhook-timestamp'];
1328
+ * const signature = headers['webhook-signature'];
1329
+ *
1330
+ * const secretStr = secret.startsWith('whsec_') ? secret.slice(6) : secret;
1331
+ * const keyBytes = Buffer.from(secretStr, 'base64');
1332
+ * const signedContent = `${msgId}.${timestamp}.${rawBody}`;
1245
1333
  * const expected = crypto
1246
- * .createHmac('sha256', signingSecret)
1247
- * .update(message)
1248
- * .digest('hex');
1249
- * return crypto.timingSafeEqual(
1250
- * Buffer.from(expected),
1251
- * Buffer.from(signature)
1252
- * );
1334
+ * .createHmac('sha256', keyBytes)
1335
+ * .update(signedContent)
1336
+ * .digest('base64');
1337
+ *
1338
+ * return signature.split(' ').some(sig => {
1339
+ * if (!sig.startsWith('v1,')) return false;
1340
+ * try {
1341
+ * return crypto.timingSafeEqual(
1342
+ * Buffer.from(expected, 'base64'),
1343
+ * Buffer.from(sig.slice(3), 'base64')
1344
+ * );
1345
+ * } catch { return false; }
1346
+ * });
1253
1347
  * }
1254
1348
  * ```
1255
1349
  *
@@ -1398,7 +1492,7 @@ export declare namespace LinqAPIV3 {
1398
1492
  type ChatTypingIndicatorStartedWebhookEvent as ChatTypingIndicatorStartedWebhookEvent,
1399
1493
  type ChatTypingIndicatorStoppedWebhookEvent as ChatTypingIndicatorStoppedWebhookEvent,
1400
1494
  type PhoneNumberStatusUpdatedWebhookEvent as PhoneNumberStatusUpdatedWebhookEvent,
1401
- type EventsWebhookEvent as EventsWebhookEvent,
1495
+ type UnwrapWebhookEvent as UnwrapWebhookEvent,
1402
1496
  };
1403
1497
 
1404
1498
  export {
@@ -3,6 +3,8 @@
3
3
  import { APIResource } from '../../core/resource';
4
4
  import * as MessagesAPI from '../messages';
5
5
  import * as Shared from '../shared';
6
+ import * as LocationAPI from './location';
7
+ import { GetChatLocationResponse, Location, LocationRequestResponse } from './location';
6
8
  import * as ChatsMessagesAPI from './messages';
7
9
  import { MessageListParams, MessageSendParams, MessageSendResponse, Messages, SentMessage } from './messages';
8
10
  import * as ParticipantsAPI from './participants';
@@ -25,6 +27,7 @@ export class Chats extends APIResource {
25
27
  participants: ParticipantsAPI.Participants = new ParticipantsAPI.Participants(this._client);
26
28
  typing: TypingAPI.Typing = new TypingAPI.Typing(this._client);
27
29
  messages: ChatsMessagesAPI.Messages = new ChatsMessagesAPI.Messages(this._client);
30
+ location: LocationAPI.Location = new LocationAPI.Location(this._client);
28
31
 
29
32
  /**
30
33
  * Create a new chat with specified participants and send an initial message. The
@@ -817,6 +820,7 @@ export interface ChatSendVoicememoParams {
817
820
  Chats.Participants = Participants;
818
821
  Chats.Typing = Typing;
819
822
  Chats.Messages = Messages;
823
+ Chats.Location = Location;
820
824
 
821
825
  export declare namespace Chats {
822
826
  export {
@@ -853,4 +857,10 @@ export declare namespace Chats {
853
857
  type MessageSendParams as MessageSendParams,
854
858
  type MessageListParams as MessageListParams,
855
859
  };
860
+
861
+ export {
862
+ Location as Location,
863
+ type GetChatLocationResponse as GetChatLocationResponse,
864
+ type LocationRequestResponse as LocationRequestResponse,
865
+ };
856
866
  }
@@ -17,6 +17,7 @@ export {
17
17
  type ChatSendVoicememoParams,
18
18
  type ChatsListChatsPagination,
19
19
  } from './chats';
20
+ export { Location, type GetChatLocationResponse, type LocationRequestResponse } from './location';
20
21
  export {
21
22
  Messages,
22
23
  type SentMessage,
@@ -0,0 +1,138 @@
1
+ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
+
3
+ import { APIResource } from '../../core/resource';
4
+ import { APIPromise } from '../../core/api-promise';
5
+ import { buildHeaders } from '../../internal/headers';
6
+ import { RequestOptions } from '../../internal/request-options';
7
+ import { path } from '../../internal/utils/path';
8
+
9
+ /**
10
+ * Request and retrieve real-time location data via iMessage.
11
+ *
12
+ * Use these endpoints to request a contact's location, retrieve location data
13
+ * for contacts who are sharing with you, and subscribe to webhooks when someone
14
+ * starts or stops sharing their location.
15
+ *
16
+ * **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format:
17
+ * `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available.
18
+ */
19
+ export class Location extends APIResource {
20
+ /**
21
+ * Send a location sharing request to a contact. They will receive an iMessage
22
+ * prompt asking them to share their location.
23
+ *
24
+ * Location requests only work in **1:1 iMessage chats** (Apple limitation).
25
+ * Attempting to request location in a group chat, or in an SMS or RCS chat,
26
+ * returns `409` (Operation not supported on this chat's service type).
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const locationRequestResponse =
31
+ * await client.chats.location.request(
32
+ * '975d0776-bd17-4273-8337-f346b4c661b0',
33
+ * );
34
+ * ```
35
+ */
36
+ request(chatID: string, options?: RequestOptions): APIPromise<LocationRequestResponse> {
37
+ return this._client.post(path`/v3/chats/${chatID}/location/request`, options);
38
+ }
39
+
40
+ /**
41
+ * Retrieve the current location for contacts sharing with you in a chat.
42
+ *
43
+ * Returns a [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946)
44
+ * `FeatureCollection` with a `Feature` for each participant actively sharing their
45
+ * location.
46
+ *
47
+ * Works for both 1:1 and group chats. In group chats, returns a separate feature
48
+ * for each participant who is sharing. Each feature's `properties.handle`
49
+ * identifies the user.
50
+ *
51
+ * Returns an empty `features` array if no one is sharing or no location data is
52
+ * available yet.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const getChatLocationResponse =
57
+ * await client.chats.location.retrieve(
58
+ * '975d0776-bd17-4273-8337-f346b4c661b0',
59
+ * );
60
+ * ```
61
+ */
62
+ retrieve(chatID: string, options?: RequestOptions): APIPromise<GetChatLocationResponse> {
63
+ return this._client.get(path`/v3/chats/${chatID}/location`, {
64
+ ...options,
65
+ headers: buildHeaders([{ Accept: 'application/geo+json' }, options?.headers]),
66
+ });
67
+ }
68
+ }
69
+
70
+ export interface GetChatLocationResponse {
71
+ data: GetChatLocationResponse.Data;
72
+
73
+ success: boolean;
74
+ }
75
+
76
+ export namespace GetChatLocationResponse {
77
+ export interface Data {
78
+ features: Array<Data.Feature>;
79
+
80
+ type: 'FeatureCollection';
81
+ }
82
+
83
+ export namespace Data {
84
+ export interface Feature {
85
+ geometry: Feature.Geometry;
86
+
87
+ properties: Feature.Properties;
88
+
89
+ type: 'Feature';
90
+ }
91
+
92
+ export namespace Feature {
93
+ export interface Geometry {
94
+ /**
95
+ * [longitude, latitude] or [longitude, latitude, altitude]
96
+ */
97
+ coordinates: Array<number>;
98
+
99
+ type: 'Point';
100
+ }
101
+
102
+ export interface Properties {
103
+ /**
104
+ * Phone number or email of the person sharing their location
105
+ */
106
+ handle: string;
107
+
108
+ /**
109
+ * Full street address
110
+ */
111
+ address?: string;
112
+
113
+ /**
114
+ * City or locality name
115
+ */
116
+ locality?: string;
117
+
118
+ /**
119
+ * When the location was last updated
120
+ */
121
+ updated_at?: string;
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ export interface LocationRequestResponse {
128
+ message: string;
129
+
130
+ success: boolean;
131
+ }
132
+
133
+ export declare namespace Location {
134
+ export {
135
+ type GetChatLocationResponse as GetChatLocationResponse,
136
+ type LocationRequestResponse as LocationRequestResponse,
137
+ };
138
+ }
@@ -138,10 +138,11 @@ export interface SentMessage {
138
138
  /**
139
139
  * Current delivery status of a message
140
140
  */
141
- delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'failed';
141
+ delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed';
142
142
 
143
143
  /**
144
- * Whether the message has been read
144
+ * @deprecated DEPRECATED: Use `delivery_status == "read"` instead. Whether the
145
+ * message has been read.
145
146
  */
146
147
  is_read: boolean;
147
148
 
@@ -88,5 +88,5 @@ export {
88
88
  type ChatTypingIndicatorStartedWebhookEvent,
89
89
  type ChatTypingIndicatorStoppedWebhookEvent,
90
90
  type PhoneNumberStatusUpdatedWebhookEvent,
91
- type EventsWebhookEvent,
91
+ type UnwrapWebhookEvent,
92
92
  } from './webhooks';
@@ -159,7 +159,14 @@ export interface Message {
159
159
  created_at: string;
160
160
 
161
161
  /**
162
- * Whether the message has been delivered
162
+ * Current delivery status of a message
163
+ */
164
+ delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed';
165
+
166
+ /**
167
+ * @deprecated DEPRECATED: Use `delivery_status` instead (true when
168
+ * `delivery_status` is `delivered` or `read`). Whether the message has been
169
+ * delivered.
163
170
  */
164
171
  is_delivered: boolean;
165
172
 
@@ -169,7 +176,8 @@ export interface Message {
169
176
  is_from_me: boolean;
170
177
 
171
178
  /**
172
- * Whether the message has been read
179
+ * @deprecated DEPRECATED: Use `delivery_status == "read"` instead. Whether the
180
+ * message has been read.
173
181
  */
174
182
  is_read: boolean;
175
183