@reqvet-sdk/sdk 2.2.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.
- package/CHANGELOG.md +23 -0
- package/README.md +179 -0
- package/SDK_REFERENCE.md +516 -0
- package/SECURITY.md +119 -0
- package/package.json +42 -0
- package/src/index.d.ts +261 -0
- package/src/index.js +444 -0
- package/src/webhooks.d.ts +14 -0
- package/src/webhooks.js +52 -0
package/SDK_REFERENCE.md
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
# @reqvet/sdk — Technical Reference
|
|
2
|
+
|
|
3
|
+
Complete parameter and response documentation for all SDK methods.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1) Instantiation
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import ReqVet from '@reqvet/sdk';
|
|
11
|
+
|
|
12
|
+
const reqvet = new ReqVet(process.env.REQVET_API_KEY!, {
|
|
13
|
+
baseUrl: process.env.REQVET_BASE_URL ?? 'https://api.reqvet.com',
|
|
14
|
+
pollInterval: 5000, // polling interval in ms (default: 5000)
|
|
15
|
+
timeout: 5 * 60 * 1000, // max polling wait in ms (default: 300 000 = 5 min)
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The API key must start with `rqv_`. An `Error` is thrown immediately if it doesn't.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2) Before your first call
|
|
24
|
+
|
|
25
|
+
### Get your credentials
|
|
26
|
+
|
|
27
|
+
Your ReqVet account manager will provide:
|
|
28
|
+
- `REQVET_API_KEY` — your org API key (`rqv_live_...`)
|
|
29
|
+
- `REQVET_BASE_URL` — the API base URL
|
|
30
|
+
- `REQVET_WEBHOOK_SECRET` — your webhook signing secret (if using webhooks)
|
|
31
|
+
|
|
32
|
+
### Discover your templates
|
|
33
|
+
|
|
34
|
+
Every job requires a `templateId`. Before generating reports, list the templates available to your organization:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const { custom, system } = await reqvet.listTemplates();
|
|
38
|
+
// system = templates created by ReqVet, available to all organizations (read-only)
|
|
39
|
+
// custom = templates created by your organization
|
|
40
|
+
const templateId = system[0].id; // or custom[0].id
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 3) Integration patterns
|
|
46
|
+
|
|
47
|
+
### A) Webhook-first (recommended for production)
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
uploadAudio() → createJob({ callbackUrl }) → ReqVet POSTs result to your endpoint
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The user can close the browser — the result arrives on your server asynchronously.
|
|
54
|
+
|
|
55
|
+
### B) Polling (development / simple integrations)
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
uploadAudio() → createJob() → waitForJob() → report
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or use the convenience wrapper:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const report = await reqvet.generateReport({ audio, animalName, templateId, waitForResult: true });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 4) Methods
|
|
70
|
+
|
|
71
|
+
### `uploadAudio(audio, fileName?)`
|
|
72
|
+
|
|
73
|
+
Upload an audio file to ReqVet storage.
|
|
74
|
+
|
|
75
|
+
**Parameters:**
|
|
76
|
+
| Name | Type | Required | Description |
|
|
77
|
+
|------|------|----------|-------------|
|
|
78
|
+
| `audio` | `Blob \| File \| Buffer` | ✅ | Audio data |
|
|
79
|
+
| `fileName` | `string` | — | File name, used to infer MIME type (default: `audio.webm`) |
|
|
80
|
+
|
|
81
|
+
**Response:**
|
|
82
|
+
```ts
|
|
83
|
+
{
|
|
84
|
+
audio_file: string; // canonical storage path — pass this to createJob()
|
|
85
|
+
path: string; // alias of audio_file
|
|
86
|
+
size_bytes: number;
|
|
87
|
+
content_type: string;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Supported formats: `mp3`, `wav`, `webm`, `ogg`, `m4a`, `aac`, `flac`. Max size: 100 MB.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### `generateReport(params)`
|
|
96
|
+
|
|
97
|
+
Convenience wrapper: `uploadAudio → createJob`. Optionally waits for completion.
|
|
98
|
+
|
|
99
|
+
**Parameters:**
|
|
100
|
+
| Name | Type | Required | Description |
|
|
101
|
+
|------|------|----------|-------------|
|
|
102
|
+
| `audio` | `Blob \| File \| Buffer` | ✅ | Audio data |
|
|
103
|
+
| `animalName` | `string` | ✅ | Name of the animal |
|
|
104
|
+
| `templateId` | `string` | ✅ | Template UUID (from `listTemplates()`) |
|
|
105
|
+
| `fileName` | `string` | — | File name |
|
|
106
|
+
| `callbackUrl` | `string` | — | Your webhook endpoint (HTTPS, publicly reachable) |
|
|
107
|
+
| `metadata` | `Record<string, unknown>` | — | Passthrough data (e.g. `{ consultationId, vetId }`) |
|
|
108
|
+
| `extraInstructions` | `string` | — | Additional generation instructions injected into the prompt |
|
|
109
|
+
| `waitForResult` | `boolean` | — | If `true`, polls and returns the final report. Default: `false` |
|
|
110
|
+
| `onStatus` | `(status: string) => void` | — | Called on each poll (only when `waitForResult: true`) |
|
|
111
|
+
|
|
112
|
+
**Response:**
|
|
113
|
+
- `waitForResult: false` (default): `{ job_id: string, status: 'pending' }`
|
|
114
|
+
- `waitForResult: true`: `ReqVetReport` (see `waitForJob`)
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### `createJob(params)`
|
|
119
|
+
|
|
120
|
+
Start a transcription + report generation pipeline.
|
|
121
|
+
|
|
122
|
+
**Parameters:**
|
|
123
|
+
| Name | Type | Required | Description |
|
|
124
|
+
|------|------|----------|-------------|
|
|
125
|
+
| `audioFile` | `string` | ✅ | Value of `uploadAudio().path` |
|
|
126
|
+
| `animalName` | `string` | ✅ | Name of the animal |
|
|
127
|
+
| `templateId` | `string` | ✅ | Template UUID |
|
|
128
|
+
| `callbackUrl` | `string` | — | Webhook URL (HTTPS, publicly reachable). Falls back to the org default webhook if omitted. |
|
|
129
|
+
| `metadata` | `Record<string, unknown>` | — | Passthrough data — correlate with your own records |
|
|
130
|
+
| `extraInstructions` | `string` | — | Extra generation instructions (max 5 000 chars) |
|
|
131
|
+
|
|
132
|
+
**Response:**
|
|
133
|
+
```ts
|
|
134
|
+
{ job_id: string; status: 'pending' }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> **Rate limit**: 10 000 requests/minute per organization.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### `listJobs(options?)`
|
|
142
|
+
|
|
143
|
+
List jobs for the authenticated organization, with pagination and filtering.
|
|
144
|
+
|
|
145
|
+
**Parameters:**
|
|
146
|
+
| Name | Type | Default | Description |
|
|
147
|
+
|------|------|---------|-------------|
|
|
148
|
+
| `limit` | `number` | `20` | Results per page (1–100) |
|
|
149
|
+
| `offset` | `number` | `0` | Pagination offset |
|
|
150
|
+
| `status` | `string` | — | Filter: `pending` `transcribing` `generating` `completed` `failed` `amending` |
|
|
151
|
+
| `sort` | `string` | `created_at` | Sort field: `created_at` or `updated_at` |
|
|
152
|
+
| `order` | `string` | `desc` | Direction: `asc` or `desc` |
|
|
153
|
+
|
|
154
|
+
**Response:**
|
|
155
|
+
```ts
|
|
156
|
+
{
|
|
157
|
+
jobs: JobSummary[];
|
|
158
|
+
pagination: { total: number; limit: number; offset: number; has_more: boolean };
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### `getJob(jobId)`
|
|
165
|
+
|
|
166
|
+
Get the current state and result of a job.
|
|
167
|
+
|
|
168
|
+
**Response fields by status:**
|
|
169
|
+
|
|
170
|
+
| Field | `pending` | `transcribing` | `generating` | `completed` | `failed` |
|
|
171
|
+
|-------|:---------:|:--------------:|:------------:|:-----------:|:--------:|
|
|
172
|
+
| `job_id` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
173
|
+
| `status` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
174
|
+
| `animal_name` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
175
|
+
| `metadata` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
176
|
+
| `transcription` | — | — | ✅ | ✅ | — |
|
|
177
|
+
| `result.html` | — | — | — | ✅ | — |
|
|
178
|
+
| `result.fields` | — | — | — | ✅* | — |
|
|
179
|
+
| `cost` | — | — | — | ✅ | — |
|
|
180
|
+
| `error` | — | — | — | — | ✅ |
|
|
181
|
+
|
|
182
|
+
*`result.fields` is only present if your organization has a `field_schema` configured (structured data extraction). It is `null` otherwise. See [Field schema](#field-schema) below.
|
|
183
|
+
|
|
184
|
+
**Cost structure (completed jobs):**
|
|
185
|
+
```ts
|
|
186
|
+
cost: {
|
|
187
|
+
transcription_usd: number;
|
|
188
|
+
generation_usd: number;
|
|
189
|
+
total_usd: number;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
> **Note**: `cost` is available via `getJob()` and `waitForJob()`, but is **not** included in webhook payloads. Retrieve it with `getJob()` after receiving a `job.completed` event if needed.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### `waitForJob(jobId, onStatus?)`
|
|
198
|
+
|
|
199
|
+
Poll until a job reaches `completed` or `failed`. Respects `pollInterval` and `timeout`.
|
|
200
|
+
|
|
201
|
+
**Response (`ReqVetReport`):**
|
|
202
|
+
```ts
|
|
203
|
+
{
|
|
204
|
+
jobId: string;
|
|
205
|
+
html: string; // generated report HTML
|
|
206
|
+
fields: ExtractedFields | null; // null if no field_schema configured
|
|
207
|
+
transcription: string;
|
|
208
|
+
animalName: string;
|
|
209
|
+
cost: { transcription_usd: number; generation_usd: number; total_usd: number };
|
|
210
|
+
metadata: Record<string, unknown>;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Throws `ReqVetError` if the job fails or the timeout is exceeded.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### `regenerateJob(jobId, options?)`
|
|
219
|
+
|
|
220
|
+
Regenerate the report for a completed job — e.g. with different instructions or a different template.
|
|
221
|
+
|
|
222
|
+
**Parameters:**
|
|
223
|
+
| Name | Type | Description |
|
|
224
|
+
|------|------|-------------|
|
|
225
|
+
| `extraInstructions` | `string` | New instructions (max 2 000 chars) |
|
|
226
|
+
| `templateId` | `string` | Switch to a different template |
|
|
227
|
+
|
|
228
|
+
**Response:**
|
|
229
|
+
```ts
|
|
230
|
+
{ job_id: string; status: 'completed'; result: { html: string; fields?: ExtractedFields } }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Triggers a `job.regenerated` webhook event if a `callbackUrl` is configured.
|
|
234
|
+
|
|
235
|
+
> **Rate limit**: 30 requests/minute per organization.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### `amendJob(jobId, params)`
|
|
240
|
+
|
|
241
|
+
Add an audio complement to a completed job. The new audio is transcribed, merged with the existing transcription, and the report is regenerated.
|
|
242
|
+
|
|
243
|
+
**Parameters:**
|
|
244
|
+
| Name | Type | Required | Description |
|
|
245
|
+
|------|------|----------|-------------|
|
|
246
|
+
| `audioFile` | `string` | ✅ | Value of `uploadAudio().path` |
|
|
247
|
+
| `templateId` | `string` | — | Switch to a different template |
|
|
248
|
+
|
|
249
|
+
**Response:**
|
|
250
|
+
```ts
|
|
251
|
+
{ job_id: string; status: 'amending'; amendment_number: number; message: string }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The job returns to `completed` when the amendment finishes. Use `waitForJob()` or listen for the `job.amended` webhook event. Multiple amendments are supported — each one appends to the full transcription.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### `reformulateReport(jobId, params)`
|
|
259
|
+
|
|
260
|
+
Generate an alternative version of a completed report for a specific audience.
|
|
261
|
+
|
|
262
|
+
**Parameters:**
|
|
263
|
+
| Name | Type | Required | Description |
|
|
264
|
+
|------|------|----------|-------------|
|
|
265
|
+
| `purpose` | `string` | ✅ | `owner` `referral` `summary` `custom` `diagnostic_hypothesis` |
|
|
266
|
+
| `customInstructions` | `string` | If `purpose: 'custom'` | Reformulation instructions |
|
|
267
|
+
|
|
268
|
+
**Purpose values:**
|
|
269
|
+
| Value | Output |
|
|
270
|
+
|-------|--------|
|
|
271
|
+
| `owner` | Simplified version for the pet owner |
|
|
272
|
+
| `referral` | Clinical summary for a specialist |
|
|
273
|
+
| `summary` | Short internal note |
|
|
274
|
+
| `diagnostic_hypothesis` | Differential diagnosis list |
|
|
275
|
+
| `custom` | Defined by `customInstructions` |
|
|
276
|
+
|
|
277
|
+
**Response (`ReqVetReformulation`):**
|
|
278
|
+
```ts
|
|
279
|
+
{
|
|
280
|
+
id: string;
|
|
281
|
+
job_id: string;
|
|
282
|
+
purpose: string;
|
|
283
|
+
html: string;
|
|
284
|
+
custom_instructions?: string;
|
|
285
|
+
cost: { model: string; prompt_tokens: number; completion_tokens: number; cost_usd: number };
|
|
286
|
+
created_at: string;
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
> **Rate limit**: 30 requests/minute per organization.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### `listReformulations(jobId)`
|
|
295
|
+
|
|
296
|
+
**Response:** `{ reformulations: ReqVetReformulation[] }`
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### Templates
|
|
301
|
+
|
|
302
|
+
#### `listTemplates()` → `{ custom: Template[], system: Template[] }`
|
|
303
|
+
|
|
304
|
+
- **`system`** — templates created by ReqVet, visible to all organizations. Read-only. Start here to find available `templateId` values.
|
|
305
|
+
- **`custom`** — templates created by your organization. Editable via `createTemplate` / `updateTemplate`.
|
|
306
|
+
|
|
307
|
+
#### `getTemplate(templateId)` → `Template`
|
|
308
|
+
|
|
309
|
+
#### `createTemplate(params)` → `Template`
|
|
310
|
+
|
|
311
|
+
| Name | Type | Required |
|
|
312
|
+
|------|------|----------|
|
|
313
|
+
| `name` | `string` | ✅ |
|
|
314
|
+
| `content` | `string` | ✅ |
|
|
315
|
+
| `description` | `string` | — |
|
|
316
|
+
| `is_default` | `boolean` | — |
|
|
317
|
+
|
|
318
|
+
#### `updateTemplate(templateId, updates)` → `Template`
|
|
319
|
+
|
|
320
|
+
All fields optional (partial update). Same fields as `createTemplate`.
|
|
321
|
+
|
|
322
|
+
#### `deleteTemplate(templateId)` → `{ success: true }`
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### `health()`
|
|
327
|
+
|
|
328
|
+
**Response:** `{ status: 'ok' | 'degraded'; timestamp: string }`
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## 5) Field schema
|
|
333
|
+
|
|
334
|
+
If your organization has a `field_schema` configured, ReqVet extracts structured fields from each consultation in addition to generating the HTML report.
|
|
335
|
+
|
|
336
|
+
Example `result.fields` for a standard checkup:
|
|
337
|
+
|
|
338
|
+
```json
|
|
339
|
+
{
|
|
340
|
+
"espece": "Chien",
|
|
341
|
+
"race": "Labrador",
|
|
342
|
+
"poids": 28.5,
|
|
343
|
+
"temperature": 38.6,
|
|
344
|
+
"traitements": ["Frontline", "Milbemax"],
|
|
345
|
+
"sterilise": true,
|
|
346
|
+
"prochain_rdv": "Dans 6 mois"
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`fields` is `null` if no `field_schema` is configured for your organization. Contact your ReqVet account manager to enable and configure structured extraction.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 6) Webhook events
|
|
355
|
+
|
|
356
|
+
ReqVet POSTs to your `callbackUrl` when a job changes state. All events share the same format.
|
|
357
|
+
|
|
358
|
+
### Headers
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
Content-Type: application/json
|
|
362
|
+
X-ReqVet-Signature: sha256=<hex> (only if org has a webhook_secret)
|
|
363
|
+
X-ReqVet-Timestamp: <unix_ms> (only if org has a webhook_secret)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Event types and payloads
|
|
367
|
+
|
|
368
|
+
#### `job.completed`
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
{
|
|
372
|
+
"event": "job.completed",
|
|
373
|
+
"job_id": "a1b2c3d4-...",
|
|
374
|
+
"animal_name": "Rex",
|
|
375
|
+
"transcription": "Le vétérinaire examine Rex, labrador de 5 ans...",
|
|
376
|
+
"html": "<section class=\"cr\">...</section>",
|
|
377
|
+
"fields": { "espece": "Chien", "poids": 28.5 },
|
|
378
|
+
"metadata": { "consultationId": "abc123", "vetId": "v42" }
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
> `fields` is absent if the organization has no `field_schema`. `cost` is not in the webhook — retrieve it with `getJob()` if needed.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
#### `job.failed`
|
|
387
|
+
|
|
388
|
+
```json
|
|
389
|
+
{
|
|
390
|
+
"event": "job.failed",
|
|
391
|
+
"job_id": "a1b2c3d4-...",
|
|
392
|
+
"animal_name": "Rex",
|
|
393
|
+
"error": "Transcription failed",
|
|
394
|
+
"metadata": { "consultationId": "abc123" }
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
#### `job.amended`
|
|
401
|
+
|
|
402
|
+
Sent when an amendment (`amendJob`) completes successfully.
|
|
403
|
+
|
|
404
|
+
```json
|
|
405
|
+
{
|
|
406
|
+
"event": "job.amended",
|
|
407
|
+
"job_id": "a1b2c3d4-...",
|
|
408
|
+
"animal_name": "Rex",
|
|
409
|
+
"transcription": "...full transcription including amendment...",
|
|
410
|
+
"html": "<section class=\"cr\">...</section>",
|
|
411
|
+
"amendment_number": 1,
|
|
412
|
+
"fields": { "espece": "Chien", "poids": 28.5 },
|
|
413
|
+
"metadata": { "consultationId": "abc123" }
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
#### `job.amend_failed`
|
|
420
|
+
|
|
421
|
+
Sent when amendment transcription fails. The original report is preserved.
|
|
422
|
+
|
|
423
|
+
```json
|
|
424
|
+
{
|
|
425
|
+
"event": "job.amend_failed",
|
|
426
|
+
"job_id": "a1b2c3d4-...",
|
|
427
|
+
"animal_name": "Rex",
|
|
428
|
+
"error": "Amendment transcription failed",
|
|
429
|
+
"metadata": { "consultationId": "abc123" }
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
#### `job.regenerated`
|
|
436
|
+
|
|
437
|
+
Sent when `regenerateJob()` completes.
|
|
438
|
+
|
|
439
|
+
```json
|
|
440
|
+
{
|
|
441
|
+
"event": "job.regenerated",
|
|
442
|
+
"job_id": "a1b2c3d4-...",
|
|
443
|
+
"animal_name": "Rex",
|
|
444
|
+
"html": "<section class=\"cr\">...</section>",
|
|
445
|
+
"fields": { "espece": "Chien", "poids": 28.5 },
|
|
446
|
+
"metadata": { "consultationId": "abc123" }
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
### Retry policy
|
|
453
|
+
|
|
454
|
+
ReqVet retries failed webhook deliveries **3 times** with delays of 0s, 2s, and 5s. After 3 failures, the event is marked as undelivered. Implement idempotency in your handler (deduplicate on `job_id + event`).
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 7) Webhook verification
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
import { verifyWebhookSignature } from '@reqvet/sdk/webhooks';
|
|
462
|
+
|
|
463
|
+
const { ok, reason } = verifyWebhookSignature({
|
|
464
|
+
secret: process.env.REQVET_WEBHOOK_SECRET!,
|
|
465
|
+
rawBody, // raw request body string — read BEFORE JSON.parse
|
|
466
|
+
signature, // X-ReqVet-Signature header value
|
|
467
|
+
timestamp, // X-ReqVet-Timestamp header value
|
|
468
|
+
maxSkewMs: 5 * 60 * 1000, // reject events older than 5 min (default)
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Rejection reasons: `missing_headers` `invalid_timestamp` `stale_timestamp` `invalid_signature`
|
|
473
|
+
|
|
474
|
+
See [SECURITY.md](./SECURITY.md) for a complete Next.js implementation example.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## 8) Error handling
|
|
479
|
+
|
|
480
|
+
All methods throw `ReqVetError` on HTTP errors or network failures:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
import { ReqVetError } from '@reqvet/sdk';
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const report = await reqvet.waitForJob(jobId);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
if (err instanceof ReqVetError) {
|
|
489
|
+
console.error(err.message); // human-readable message
|
|
490
|
+
console.error(err.status); // HTTP status (0 for network/timeout errors)
|
|
491
|
+
console.error(err.body); // raw response body
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
| Status | Meaning |
|
|
497
|
+
|--------|---------|
|
|
498
|
+
| `400` | Validation error — check `err.body.issues` |
|
|
499
|
+
| `401` | Invalid or missing API key |
|
|
500
|
+
| `403` | Monthly quota exceeded |
|
|
501
|
+
| `404` | Job or template not found |
|
|
502
|
+
| `429` | Rate limit exceeded — back off and retry |
|
|
503
|
+
| `500` | ReqVet internal error |
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## 9) Integration checklist
|
|
508
|
+
|
|
509
|
+
- [ ] SDK used **server-side only** — API key never in browser bundles
|
|
510
|
+
- [ ] `listTemplates()` called at startup to discover available `templateId` values
|
|
511
|
+
- [ ] `metadata` used to correlate ReqVet jobs with your own records (`consultationId`, `vetId`, etc.)
|
|
512
|
+
- [ ] Webhook endpoint handles all 5 event types: `job.completed`, `job.failed`, `job.amended`, `job.amend_failed`, `job.regenerated`
|
|
513
|
+
- [ ] Webhook signature verified on every incoming event
|
|
514
|
+
- [ ] Timestamp anti-replay check enabled (`maxSkewMs`)
|
|
515
|
+
- [ ] Idempotency implemented — deduplicate on `job_id + event`
|
|
516
|
+
- [ ] `REQVET_API_KEY` and `REQVET_WEBHOOK_SECRET` stored in environment variables, never hardcoded
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## API key management
|
|
4
|
+
|
|
5
|
+
Your ReqVet API key (`rqv_live_...`) grants full access to your organization's data and quota. Treat it like a password.
|
|
6
|
+
|
|
7
|
+
**Never** expose it in:
|
|
8
|
+
- Client-side JavaScript (React, Vue, browser bundles)
|
|
9
|
+
- Mobile app binaries
|
|
10
|
+
- Public repositories
|
|
11
|
+
- Logs or error messages
|
|
12
|
+
|
|
13
|
+
**Always** load it from environment variables server-side:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const reqvet = new ReqVet(process.env.REQVET_API_KEY!);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Proxy pattern (required for browser integrations)
|
|
20
|
+
|
|
21
|
+
If your frontend records audio in the browser, use a server-side proxy — your backend calls ReqVet, never the browser directly:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Browser → your server (proxy) → ReqVet API
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Example proxy route (Next.js App Router):
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// app/api/reqvet/generate/route.ts
|
|
31
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
32
|
+
import ReqVet from '@reqvet/sdk';
|
|
33
|
+
|
|
34
|
+
const reqvet = new ReqVet(process.env.REQVET_API_KEY!);
|
|
35
|
+
|
|
36
|
+
export async function POST(req: NextRequest) {
|
|
37
|
+
const form = await req.formData();
|
|
38
|
+
const audio = form.get('audio') as File;
|
|
39
|
+
|
|
40
|
+
const { path } = await reqvet.uploadAudio(audio, audio.name);
|
|
41
|
+
const job = await reqvet.createJob({
|
|
42
|
+
audioFile: path,
|
|
43
|
+
animalName: form.get('animalName') as string,
|
|
44
|
+
templateId: form.get('templateId') as string,
|
|
45
|
+
callbackUrl: process.env.REQVET_WEBHOOK_URL,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return NextResponse.json(job);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Webhook signature verification
|
|
53
|
+
|
|
54
|
+
Every ReqVet webhook is signed with HMAC-SHA256. Always verify the signature before processing.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { verifyWebhookSignature } from '@reqvet/sdk/webhooks';
|
|
58
|
+
|
|
59
|
+
export async function POST(req: NextRequest) {
|
|
60
|
+
const rawBody = await req.text();
|
|
61
|
+
const signature = req.headers.get('x-reqvet-signature') ?? '';
|
|
62
|
+
const timestamp = req.headers.get('x-reqvet-timestamp') ?? '';
|
|
63
|
+
|
|
64
|
+
const { ok, reason } = verifyWebhookSignature({
|
|
65
|
+
secret: process.env.REQVET_WEBHOOK_SECRET!,
|
|
66
|
+
rawBody,
|
|
67
|
+
signature,
|
|
68
|
+
timestamp,
|
|
69
|
+
maxSkewMs: 5 * 60 * 1000, // reject events older than 5 minutes
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!ok) {
|
|
73
|
+
console.warn('Webhook rejected:', reason);
|
|
74
|
+
return new Response('Unauthorized', { status: 401 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const event = JSON.parse(rawBody);
|
|
78
|
+
// process event.job_id, event.status, event.result ...
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Signature format:**
|
|
83
|
+
- Header `X-ReqVet-Signature: sha256=<hex>`
|
|
84
|
+
- Header `X-ReqVet-Timestamp: <unix_ms>`
|
|
85
|
+
- Signed message: `HMAC_SHA256(secret, "${timestamp}.${rawBody}")`
|
|
86
|
+
|
|
87
|
+
**Rejection reasons:**
|
|
88
|
+
| `reason` | Cause |
|
|
89
|
+
|----------|-------|
|
|
90
|
+
| `missing_headers` | Signature or timestamp header absent |
|
|
91
|
+
| `invalid_timestamp` | Timestamp is not a valid number |
|
|
92
|
+
| `stale_timestamp` | Event is outside the allowed time window |
|
|
93
|
+
| `invalid_signature` | HMAC does not match |
|
|
94
|
+
|
|
95
|
+
## Idempotency
|
|
96
|
+
|
|
97
|
+
Webhooks may be delivered more than once. Use `job_id` + `event` as a deduplication key before processing:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const key = `${event.job_id}:${event.event}`;
|
|
101
|
+
if (await cache.has(key)) return new Response('OK'); // already processed
|
|
102
|
+
await cache.set(key, true, { ttl: 86400 });
|
|
103
|
+
// process...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API key rotation
|
|
107
|
+
|
|
108
|
+
If a key is compromised:
|
|
109
|
+
|
|
110
|
+
1. Generate a new key from your ReqVet admin panel
|
|
111
|
+
2. Update `REQVET_API_KEY` in your environment variables
|
|
112
|
+
3. Redeploy your application
|
|
113
|
+
4. Revoke the old key
|
|
114
|
+
|
|
115
|
+
## Responsible disclosure
|
|
116
|
+
|
|
117
|
+
If you discover a security vulnerability in this SDK or the ReqVet API, report it privately to **security@reqvet.com**. Do not open a public GitHub issue for security vulnerabilities.
|
|
118
|
+
|
|
119
|
+
We commit to acknowledging reports within 48 hours and providing a fix timeline within 7 days for critical issues.
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reqvet-sdk/sdk",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Official JavaScript SDK for the ReqVet veterinary report generation API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./webhooks": {
|
|
15
|
+
"import": "./src/webhooks.js",
|
|
16
|
+
"types": "./src/webhooks.d.ts",
|
|
17
|
+
"default": "./src/webhooks.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": ["src/", "README.md", "SDK_REFERENCE.md", "CHANGELOG.md", "SECURITY.md"],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"reqvet",
|
|
24
|
+
"veterinary",
|
|
25
|
+
"transcription",
|
|
26
|
+
"ai",
|
|
27
|
+
"sdk"
|
|
28
|
+
],
|
|
29
|
+
"author": "ReqVet <contact@reqvet.com>",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/reqvet/reqvet-sdk.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/reqvet/reqvet-sdk#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/reqvet/reqvet-sdk/issues"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|