@reqvet-sdk/sdk 2.2.2 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,7 @@ SDK JavaScript/TypeScript officiel pour l'API [ReqVet](https://reqvet.com) — g
16
16
  - **Reformuler** pour une audience spécifique — propriétaire, référé, spécialiste (`reformulateReport`)
17
17
  - **Gérer les templates** (`listTemplates`, `createTemplate`, `updateTemplate`, `deleteTemplate`)
18
18
  - **Vérifier les webhooks** avec HMAC (`@reqvet-sdk/sdk/webhooks`)
19
+ - **Provisionner et gérer des cliniques** en mode revendeur multi-tenant (`createOrganization`, `listOrganizations`, `updateOrganization`, `deactivateOrganization`)
19
20
 
20
21
  > **Note** : ce SDK n'inclut pas d'enregistreur audio. Votre application gère l'enregistrement et passe un `File`, `Blob` ou `Buffer` au SDK.
21
22
 
@@ -141,6 +142,8 @@ export async function POST(req: NextRequest) {
141
142
 
142
143
  ## API
143
144
 
145
+ ### Génération de comptes rendus
146
+
144
147
  | Méthode | Description |
145
148
  |---------|-------------|
146
149
  | `getSignedUploadUrl(fileName, contentType)` | URL signée Supabase pour upload direct (recommandé serveur) |
@@ -161,6 +164,18 @@ export async function POST(req: NextRequest) {
161
164
  | `deleteTemplate(templateId)` | Supprimer un template |
162
165
  | `health()` | Vérification de l'état de l'API |
163
166
 
167
+ ### API Partenaire / Revendeur
168
+
169
+ > Ces méthodes nécessitent une clé API revendeur (`rqv_live_...` avec `role='reseller'`), distincte de la clé d'une clinique standard. Contactez votre responsable de compte ReqVet pour obtenir une clé revendeur.
170
+
171
+ | Méthode | Description |
172
+ |---------|-------------|
173
+ | `listOrganizations()` | Lister toutes les cliniques provisionnées par le revendeur |
174
+ | `createOrganization(params)` | Provisionner une nouvelle clinique (génère sa clé API et son webhook secret) |
175
+ | `getOrganization(orgId)` | Obtenir le détail et l'usage du mois d'une clinique |
176
+ | `updateOrganization(orgId, updates)` | Modifier le quota, le statut ou le webhook d'une clinique |
177
+ | `deactivateOrganization(orgId)` | Suspendre une clinique et révoquer ses clés API (soft delete) |
178
+
164
179
  ## Événements webhook
165
180
 
166
181
  ReqVet déclenche 5 types d'événements : `job.completed`, `job.failed`, `job.amended`, `job.amend_failed`, `job.regenerated`.
@@ -169,6 +184,56 @@ Les livraisons échouées sont retentées 3 fois (0s, 2s, 5s). Implémentez l'id
169
184
 
170
185
  Voir [SDK_REFERENCE.md §6](./SDK_REFERENCE.md#6-webhook-events) pour la structure complète des payloads de chaque événement.
171
186
 
187
+ ## Intégration revendeur (multi-tenant)
188
+
189
+ Si vous êtes un éditeur logiciel intégrant ReqVet pour vos clients (cliniques), utilisez une clé API revendeur pour provisionner et gérer les organisations de manière programmatique.
190
+
191
+ ```ts
192
+ import ReqVet from '@reqvet-sdk/sdk';
193
+
194
+ // Instancier avec la clé revendeur (role='reseller')
195
+ const reseller = new ReqVet(process.env.REQVET_RESELLER_KEY!);
196
+
197
+ // Provisionner une clinique à l'onboarding
198
+ const result = await reseller.createOrganization({
199
+ name: 'Clinique du Parc',
200
+ contactEmail: 'contact@clinique-du-parc.fr',
201
+ externalId: 'votre_id_interne_4892', // votre ID — garantit l'idempotence
202
+ monthlyQuota: 500,
203
+ webhookUrl: 'https://votre-app.com/webhooks/reqvet',
204
+ });
205
+
206
+ // ⚠️ Stocker immédiatement ces valeurs — elles ne sont retournées qu'une seule fois
207
+ await db.saveClinicCredentials({
208
+ clinicId: result.organization.id,
209
+ apiKey: result.api_key, // rqv_live_...
210
+ webhookSecret: result.webhook_secret, // whsec_...
211
+ });
212
+
213
+ // La clinique utilise ensuite son propre client ReqVet avec sa clé
214
+ const clinic = new ReqVet(result.api_key);
215
+ const { system } = await clinic.listTemplates();
216
+ const job = await clinic.createJob({ audioFile, animalName, templateId: system[0].id });
217
+
218
+ // Lister toutes les cliniques avec leur usage du mois
219
+ const { organizations } = await reseller.listOrganizations();
220
+ // [{ id, name, is_active, monthly_quota, usage: { jobs_this_month, quota_remaining } }]
221
+
222
+ // Modifier le quota d'une clinique
223
+ await reseller.updateOrganization(orgId, { monthlyQuota: 1000 });
224
+
225
+ // Suspendre une clinique (révoque aussi ses clés API)
226
+ await reseller.updateOrganization(orgId, { isActive: false });
227
+
228
+ // Réactiver
229
+ await reseller.updateOrganization(orgId, { isActive: true });
230
+
231
+ // Désactiver définitivement (soft delete — données conservées pour le RGPD)
232
+ await reseller.deactivateOrganization(orgId);
233
+ ```
234
+
235
+ > **Idempotence** : si `createOrganization` est appelé plusieurs fois avec le même `externalId`, l'organisation existante est retournée sans créer de doublon. Utile pour rendre votre processus d'onboarding sûr en cas de retry.
236
+
172
237
  ## TypeScript
173
238
 
174
239
  Définitions TypeScript complètes incluses :
@@ -181,6 +246,12 @@ import type {
181
246
  Template,
182
247
  ReqVetReformulation,
183
248
  ExtractedFields,
249
+ // Partner / Reseller
250
+ PartnerOrganization,
251
+ OrganizationUsage,
252
+ CreateOrganizationParams,
253
+ UpdateOrganizationParams,
254
+ CreateOrganizationResult,
184
255
  } from '@reqvet-sdk/sdk';
185
256
  ```
186
257
 
package/SDK_REFERENCE.md CHANGED
@@ -570,7 +570,227 @@ try {
570
570
 
571
571
  ---
572
572
 
573
- ## 9) Checklist d'intégration
573
+ ## 9) API Partenaire / Revendeur
574
+
575
+ Ces méthodes sont réservées aux revendeurs (éditeurs logiciels) qui provisionnent et administrent des cliniques clientes. Elles nécessitent une clé API avec `role='reseller'`, distincte des clés d'organisation standard.
576
+
577
+ > **Isolation garantie** : un revendeur ne peut accéder qu'aux organisations qu'il a lui-même créées. La contrainte est appliquée en base de données (`parent_org_id = reseller.orgId`) — il n'est pas possible d'accéder aux données d'un autre revendeur, même avec un `orgId` connu.
578
+
579
+ ---
580
+
581
+ ### `listOrganizations()`
582
+
583
+ Lister toutes les organisations (cliniques) provisionnées par le revendeur, enrichies de l'usage du mois courant.
584
+
585
+ **Réponse :**
586
+
587
+ ```ts
588
+ {
589
+ organizations: Array<{
590
+ id: string;
591
+ name: string;
592
+ contact_email: string | null;
593
+ is_active: boolean;
594
+ monthly_quota: number | null;
595
+ external_id: string | null;
596
+ created_at: string;
597
+ usage: {
598
+ jobs_this_month: number;
599
+ quota_remaining: number | 'unlimited';
600
+ };
601
+ }>;
602
+ }
603
+ ```
604
+
605
+ **Exemple :**
606
+
607
+ ```ts
608
+ const reseller = new ReqVet(process.env.REQVET_RESELLER_KEY!);
609
+
610
+ const { organizations } = await reseller.listOrganizations();
611
+
612
+ for (const org of organizations) {
613
+ console.log(`${org.name} — ${org.usage.jobs_this_month} jobs ce mois / quota ${org.monthly_quota}`);
614
+ }
615
+ ```
616
+
617
+ ---
618
+
619
+ ### `createOrganization(params)`
620
+
621
+ Provisionner une nouvelle organisation (clinique) sous le compte revendeur.
622
+
623
+ Cette méthode crée automatiquement :
624
+ - l'enregistrement de l'organisation dans ReqVet
625
+ - une clé API `rqv_live_...` pour la clinique (stockée hashée côté serveur)
626
+ - un secret de signature webhook `whsec_...`
627
+
628
+ **Paramètres :**
629
+
630
+ | Nom | Type | Requis | Description |
631
+ |-----|------|--------|-------------|
632
+ | `name` | `string` | ✅ | Nom de la clinique |
633
+ | `contactEmail` | `string` | — | Email de contact |
634
+ | `externalId` | `string` | — | Votre identifiant interne (active l'idempotence — voir ci-dessous) |
635
+ | `monthlyQuota` | `number` | — | Nombre max de jobs par mois (défaut : 100, max : 10 000) |
636
+ | `webhookUrl` | `string` | — | URL de webhook pour les événements de jobs de cette clinique |
637
+
638
+ **Idempotence via `externalId`**
639
+
640
+ Si `externalId` est fourni et qu'une organisation avec ce même identifiant existe déjà sous ce revendeur, la méthode retourne l'organisation existante sans créer de doublon. Le champ `message` est alors présent dans la réponse (`'Organization already exists (idempotent)'`), et `api_key` / `webhook_secret` sont absents (ils ne peuvent pas être récupérés après la création initiale).
641
+
642
+ **Réponse (statut 201 — première création) :**
643
+
644
+ ```ts
645
+ {
646
+ organization: {
647
+ id: string;
648
+ name: string;
649
+ monthly_quota: number;
650
+ external_id: string | null;
651
+ };
652
+ api_key: string; // rqv_live_... — à stocker immédiatement, non récupérable ensuite
653
+ webhook_secret: string; // whsec_... — à stocker immédiatement, non récupérable ensuite
654
+ warning: string; // "Save api_key and webhook_secret now — they cannot be retrieved later!"
655
+ }
656
+ ```
657
+
658
+ **Réponse (statut 200 — idempotent, organisation déjà existante) :**
659
+
660
+ ```ts
661
+ {
662
+ message: 'Organization already exists (idempotent)';
663
+ organization: { id, name, monthly_quota, external_id, is_active };
664
+ // api_key et webhook_secret absents
665
+ }
666
+ ```
667
+
668
+ **Exemple :**
669
+
670
+ ```ts
671
+ const result = await reseller.createOrganization({
672
+ name: 'Clinique du Parc',
673
+ contactEmail: 'contact@clinique-du-parc.fr',
674
+ externalId: 'votre_id_interne_4892',
675
+ monthlyQuota: 500,
676
+ webhookUrl: 'https://votre-app.com/webhooks/reqvet',
677
+ });
678
+
679
+ if (!result.api_key) {
680
+ // Organisation déjà existante (idempotent) — récupérer la clé depuis votre propre stockage
681
+ const storedKey = await db.getApiKey(result.organization.id);
682
+ } else {
683
+ // Première création — stocker la clé et le secret immédiatement
684
+ await db.saveClinicCredentials({
685
+ orgId: result.organization.id,
686
+ apiKey: result.api_key,
687
+ webhookSecret: result.webhook_secret,
688
+ });
689
+ }
690
+ ```
691
+
692
+ ---
693
+
694
+ ### `getOrganization(orgId)`
695
+
696
+ Obtenir les détails et l'usage du mois courant d'une organisation spécifique.
697
+
698
+ **Paramètres :**
699
+
700
+ | Nom | Type | Requis | Description |
701
+ |-----|------|--------|-------------|
702
+ | `orgId` | `string` | ✅ | UUID de l'organisation |
703
+
704
+ **Réponse :** `PartnerOrganization` (même structure que les éléments de `listOrganizations`, avec `usage`)
705
+
706
+ **Exemple :**
707
+
708
+ ```ts
709
+ const org = await reseller.getOrganization('uuid-de-la-clinique');
710
+ console.log(`Quota restant : ${org.usage.quota_remaining}`);
711
+ ```
712
+
713
+ ---
714
+
715
+ ### `updateOrganization(orgId, updates)`
716
+
717
+ Modifier le quota mensuel, l'état d'activation, ou l'URL de webhook d'une organisation.
718
+
719
+ Tous les champs sont optionnels — seuls les champs fournis sont mis à jour.
720
+
721
+ **Paramètres :**
722
+
723
+ | Nom | Type | Description |
724
+ |-----|------|-------------|
725
+ | `orgId` | `string` | UUID de l'organisation |
726
+ | `updates.monthlyQuota` | `number` | Nouveau quota mensuel (1–10 000) |
727
+ | `updates.isActive` | `boolean` | `false` pour suspendre la clinique (révoque aussi ses clés API) |
728
+ | `updates.webhookUrl` | `string` | Nouvelle URL de webhook (`null` pour supprimer) |
729
+
730
+ > **Suspension** : passer `isActive: false` désactive l'organisation **et** révoque toutes ses clés API en cascade. Les jobs déjà en cours ne sont pas interrompus. Pour réactiver, passer `isActive: true` — les clés API restent révoquées et doivent être régénérées manuellement si nécessaire.
731
+
732
+ **Réponse :**
733
+
734
+ ```ts
735
+ {
736
+ id: string;
737
+ name: string;
738
+ is_active: boolean;
739
+ monthly_quota: number;
740
+ external_id: string | null;
741
+ }
742
+ ```
743
+
744
+ **Exemples :**
745
+
746
+ ```ts
747
+ // Augmenter le quota
748
+ await reseller.updateOrganization(orgId, { monthlyQuota: 1000 });
749
+
750
+ // Suspendre une clinique (non-paiement, etc.)
751
+ await reseller.updateOrganization(orgId, { isActive: false });
752
+
753
+ // Réactiver
754
+ await reseller.updateOrganization(orgId, { isActive: true });
755
+
756
+ // Mettre à jour le webhook
757
+ await reseller.updateOrganization(orgId, {
758
+ webhookUrl: 'https://votre-app.com/webhooks/reqvet/v2',
759
+ });
760
+ ```
761
+
762
+ ---
763
+
764
+ ### `deactivateOrganization(orgId)`
765
+
766
+ Désactiver définitivement une organisation et révoquer toutes ses clés API.
767
+
768
+ Il s'agit d'un **soft delete** : les données (jobs, transcriptions, comptes rendus) sont conservées en base pour respecter les obligations RGPD et permettre un audit trail. L'organisation ne peut plus créer de nouveaux jobs.
769
+
770
+ **Paramètres :**
771
+
772
+ | Nom | Type | Requis | Description |
773
+ |-----|------|--------|-------------|
774
+ | `orgId` | `string` | ✅ | UUID de l'organisation |
775
+
776
+ **Réponse :**
777
+
778
+ ```ts
779
+ { success: true; message: 'Organization and API keys deactivated' }
780
+ ```
781
+
782
+ **Exemple :**
783
+
784
+ ```ts
785
+ await reseller.deactivateOrganization(orgId);
786
+ // L'organisation est désormais inactive, ses clés API sont révoquées
787
+ ```
788
+
789
+ ---
790
+
791
+ ## 10) Checklist d'intégration
792
+
793
+ **Intégration standard (clinique)**
574
794
 
575
795
  - [ ] SDK utilisé **côté serveur uniquement** — clé API jamais dans les bundles navigateur
576
796
  - [ ] `listTemplates()` appelé au démarrage pour découvrir les `templateId` disponibles
@@ -580,3 +800,11 @@ try {
580
800
  - [ ] Vérification anti-replay du timestamp activée (`maxSkewMs`)
581
801
  - [ ] Idempotence implémentée — dédoublonnage sur `job_id + event`
582
802
  - [ ] `REQVET_API_KEY` et `REQVET_WEBHOOK_SECRET` stockés dans des variables d'environnement, jamais en dur dans le code
803
+
804
+ **Intégration revendeur (multi-tenant)**
805
+
806
+ - [ ] Clé revendeur (`REQVET_RESELLER_KEY`) distincte et stockée séparément des clés cliniques
807
+ - [ ] `externalId` systématiquement fourni à `createOrganization` pour garantir l'idempotence des onboardings
808
+ - [ ] `api_key` et `webhook_secret` retournés par `createOrganization` stockés immédiatement et de façon sécurisée — ils ne sont affichés qu'une seule fois
809
+ - [ ] Suspension de clinique gérée via `updateOrganization(orgId, { isActive: false })` (et non `deactivateOrganization` qui est irréversible)
810
+ - [ ] Usage mensuel (`usage.jobs_this_month`, `usage.quota_remaining`) consulté via `listOrganizations()` ou `getOrganization()` pour le monitoring et la facturation
package/SECURITY.md CHANGED
@@ -117,6 +117,46 @@ await cache.set(key, true, { ttl: 86400 });
117
117
  // process...
118
118
  ```
119
119
 
120
+ ## Clés revendeur et credentials cliniques
121
+
122
+ Les revendeurs manipulent deux types de secrets supplémentaires qui exigent une attention particulière.
123
+
124
+ ### Clé API revendeur
125
+
126
+ La clé revendeur (`rqv_live_...` avec `role='reseller'`) donne accès à l'ensemble des cliniques que vous administrez. Elle est plus sensible qu'une clé clinique standard.
127
+
128
+ ```bash
129
+ # Variable d'environnement dédiée, distincte de la clé clinique
130
+ REQVET_RESELLER_KEY=rqv_live_...
131
+ ```
132
+
133
+ **Ne jamais** utiliser la clé revendeur côté client ni la partager avec les cliniques.
134
+
135
+ ### `api_key` et `webhook_secret` retournés par `createOrganization`
136
+
137
+ Ces deux valeurs sont **affichées une seule fois** au moment de la création. Après la réponse HTTP initiale, elles ne peuvent pas être récupérées (seul le hash SHA-256 de la clé est stocké côté serveur).
138
+
139
+ ```ts
140
+ const result = await reseller.createOrganization({ name: 'Clinique du Parc', externalId: 'id_4892' });
141
+
142
+ // ✅ Stocker immédiatement dans votre vault ou base chiffrée
143
+ await secretsVault.store({
144
+ orgId: result.organization.id,
145
+ apiKey: result.api_key, // non récupérable après cette réponse
146
+ webhookSecret: result.webhook_secret, // non récupérable après cette réponse
147
+ });
148
+
149
+ // ✅ Transmettre la clé à la clinique de manière sécurisée (canal chiffré, jamais par email)
150
+ ```
151
+
152
+ Si une clé clinique est perdue ou compromise, le seul recours est de désactiver l'organisation via `deactivateOrganization()` et d'en créer une nouvelle.
153
+
154
+ ### Isolation entre revendeurs
155
+
156
+ L'API Partner filtre toutes les requêtes sur `parent_org_id = reseller.orgId` côté base de données. Il n'est pas possible d'accéder aux cliniques d'un autre revendeur, même en connaissant un `orgId` valide. Ne pas tenter de contourner cette isolation.
157
+
158
+ ---
159
+
120
160
  ## API key rotation
121
161
 
122
162
  If a key is compromised:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reqvet-sdk/sdk",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "Official JavaScript SDK for the ReqVet veterinary report generation API.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -18,13 +18,7 @@
18
18
  },
19
19
  "./package.json": "./package.json"
20
20
  },
21
- "files": [
22
- "src/",
23
- "README.md",
24
- "SDK_REFERENCE.md",
25
- "CHANGELOG.md",
26
- "SECURITY.md"
27
- ],
21
+ "files": ["src/", "README.md", "SDK_REFERENCE.md", "CHANGELOG.md", "SECURITY.md"],
28
22
  "keywords": [
29
23
  "reqvet",
30
24
  "veterinary",
package/src/index.d.ts CHANGED
@@ -188,6 +188,52 @@ export interface ReqVetReformulation {
188
188
  created_at: string;
189
189
  }
190
190
 
191
+ // ─── Partner / Reseller Types ────────────────────────────────
192
+
193
+ export interface OrganizationUsage {
194
+ jobs_this_month: number;
195
+ quota_remaining: number | 'unlimited';
196
+ }
197
+
198
+ export interface PartnerOrganization {
199
+ id: string;
200
+ name: string;
201
+ contact_email: string | null;
202
+ is_active: boolean;
203
+ monthly_quota: number | null;
204
+ external_id: string | null;
205
+ created_at: string;
206
+ usage?: OrganizationUsage;
207
+ }
208
+
209
+ export interface CreateOrganizationParams {
210
+ name: string;
211
+ contactEmail?: string;
212
+ /** Your internal ID — enables idempotency (same externalId returns the existing org) */
213
+ externalId?: string;
214
+ /** Max jobs per month (default: 100, max: 10 000) */
215
+ monthlyQuota?: number;
216
+ webhookUrl?: string;
217
+ }
218
+
219
+ export interface UpdateOrganizationParams {
220
+ monthlyQuota?: number;
221
+ /** Set to false to suspend the clinic and revoke its API keys */
222
+ isActive?: boolean;
223
+ webhookUrl?: string;
224
+ }
225
+
226
+ export interface CreateOrganizationResult {
227
+ organization: Pick<PartnerOrganization, 'id' | 'name' | 'monthly_quota' | 'external_id'>;
228
+ /** The clinic's API key — returned only once, store it securely! */
229
+ api_key: string;
230
+ /** Webhook signing secret — returned only once, store it securely! */
231
+ webhook_secret: string;
232
+ warning: string;
233
+ /** Returned instead of api_key/webhook_secret when the org already exists (idempotent) */
234
+ message?: string;
235
+ }
236
+
191
237
  // ─── Client ──────────────────────────────────────────────────
192
238
 
193
239
  export declare class ReqVetError extends Error {
@@ -272,6 +318,30 @@ export declare class ReqVet {
272
318
  /** Delete a template */
273
319
  deleteTemplate(templateId: string): Promise<{ success: boolean }>;
274
320
 
321
+ // ─── Partner / Reseller (requires role='reseller' API key) ──
322
+
323
+ /** List all organizations provisioned by the reseller */
324
+ listOrganizations(): Promise<{ organizations: PartnerOrganization[] }>;
325
+
326
+ /**
327
+ * Provision a new organization/clinic.
328
+ * Idempotent via externalId — returns existing org if already provisioned.
329
+ * ⚠️ api_key and webhook_secret are returned only once.
330
+ */
331
+ createOrganization(params: CreateOrganizationParams): Promise<CreateOrganizationResult>;
332
+
333
+ /** Get details and current month usage of a specific organization */
334
+ getOrganization(orgId: string): Promise<PartnerOrganization>;
335
+
336
+ /** Update an organization's quota, status, or webhook URL */
337
+ updateOrganization(
338
+ orgId: string,
339
+ updates: UpdateOrganizationParams,
340
+ ): Promise<Pick<PartnerOrganization, 'id' | 'name' | 'is_active' | 'monthly_quota' | 'external_id'>>;
341
+
342
+ /** Deactivate an organization and revoke all its API keys (soft delete) */
343
+ deactivateOrganization(orgId: string): Promise<{ success: boolean; message: string }>;
344
+
275
345
  /** Health check */
276
346
  health(): Promise<{ status: string; services: Record<string, string> }>;
277
347
  }
package/src/index.js CHANGED
@@ -416,6 +416,87 @@ class ReqVet {
416
416
  return this._fetch('DELETE', `/api/v1/templates/${templateId}`);
417
417
  }
418
418
 
419
+ // ─── Partner / Reseller ────────────────────────────────────
420
+
421
+ /**
422
+ * List all organizations provisioned by the reseller.
423
+ * Requires a reseller API key (role='reseller').
424
+ *
425
+ * @returns {Promise<{organizations: Object[]}>}
426
+ */
427
+ async listOrganizations() {
428
+ return this._fetch('GET', '/api/v1/partner/orgs');
429
+ }
430
+
431
+ /**
432
+ * Provision a new organization (clinic) under the reseller account.
433
+ * Requires a reseller API key (role='reseller').
434
+ *
435
+ * Idempotent via externalId: if an org with the same externalId already
436
+ * exists under this reseller, the existing one is returned (no duplicate).
437
+ *
438
+ * ⚠️ The returned api_key and webhook_secret are shown only once — store them securely.
439
+ *
440
+ * @param {Object} params
441
+ * @param {string} params.name - Clinic name
442
+ * @param {string} [params.contactEmail]
443
+ * @param {string} [params.externalId] - Your internal ID (enables idempotency)
444
+ * @param {number} [params.monthlyQuota] - Job quota per month (default: 100, max: 10 000)
445
+ * @param {string} [params.webhookUrl] - Webhook URL for job results
446
+ * @returns {Promise<Object>}
447
+ */
448
+ async createOrganization({ name, contactEmail, externalId, monthlyQuota, webhookUrl }) {
449
+ return this._fetch('POST', '/api/v1/partner/orgs', {
450
+ name,
451
+ contact_email: contactEmail,
452
+ external_id: externalId,
453
+ monthly_quota: monthlyQuota,
454
+ webhook_url: webhookUrl,
455
+ });
456
+ }
457
+
458
+ /**
459
+ * Get details and current month usage of a specific organization.
460
+ * Requires a reseller API key (role='reseller').
461
+ *
462
+ * @param {string} orgId
463
+ * @returns {Promise<Object>}
464
+ */
465
+ async getOrganization(orgId) {
466
+ return this._fetch('GET', `/api/v1/partner/orgs/${orgId}`);
467
+ }
468
+
469
+ /**
470
+ * Update an organization's quota, status, or webhook URL.
471
+ * Requires a reseller API key (role='reseller').
472
+ *
473
+ * @param {string} orgId
474
+ * @param {Object} updates
475
+ * @param {number} [updates.monthlyQuota]
476
+ * @param {boolean} [updates.isActive] - Set to false to suspend the clinic
477
+ * @param {string} [updates.webhookUrl]
478
+ * @returns {Promise<Object>}
479
+ */
480
+ async updateOrganization(orgId, { monthlyQuota, isActive, webhookUrl } = {}) {
481
+ return this._fetch('PATCH', `/api/v1/partner/orgs/${orgId}`, {
482
+ monthly_quota: monthlyQuota,
483
+ is_active: isActive,
484
+ webhook_url: webhookUrl,
485
+ });
486
+ }
487
+
488
+ /**
489
+ * Deactivate an organization and revoke all its API keys.
490
+ * Soft delete — data is preserved for audit/GDPR purposes.
491
+ * Requires a reseller API key (role='reseller').
492
+ *
493
+ * @param {string} orgId
494
+ * @returns {Promise<{success: boolean, message: string}>}
495
+ */
496
+ async deactivateOrganization(orgId) {
497
+ return this._fetch('DELETE', `/api/v1/partner/orgs/${orgId}`);
498
+ }
499
+
419
500
  // ─── Health ────────────────────────────────────────────────
420
501
 
421
502
  /**