@medalsocial/sdk 1.7.0 → 1.8.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.
@@ -42,6 +42,10 @@
42
42
  "name": "Bookings",
43
43
  "description": "Appointment bookings — the service catalogue, free slots, staff actions on a booking, and the customer-facing manage-token routes. Money is always integer øre."
44
44
  },
45
+ {
46
+ "name": "Portal",
47
+ "description": "Customer self-service portal — e-mail one-time-code login, then session-bound access to the signed-in contact's own profile, bookings, data export and erasure. Session routes carry `X-Portal-Session`."
48
+ },
45
49
  {
46
50
  "name": "GDPR",
47
51
  "description": "Manage exports, consent records, and cookie consent."
@@ -1504,6 +1508,245 @@
1504
1508
  }
1505
1509
  }
1506
1510
  },
1511
+ "/api/v1/portal/login/start": {
1512
+ "post": {
1513
+ "tags": [
1514
+ "Portal"
1515
+ ],
1516
+ "operationId": "startPortalLogin",
1517
+ "summary": "E-mail a one-time login code",
1518
+ "description": "Always answers 202 `{ status: \"sent\" }`, whether or not the address belongs to a contact — enumeration-safe by design. Rate-limited per address and per caller (`429 RATE_LIMITED`). Not idempotency-keyed. Errors: `400 VALIDATION_ERROR`, `403 FORBIDDEN` (key lacks `write:portal`), `429 RATE_LIMITED`.",
1519
+ "requestBody": {
1520
+ "required": true,
1521
+ "content": {
1522
+ "application/json": {
1523
+ "schema": {
1524
+ "$ref": "#/components/schemas/PortalLoginStartInput"
1525
+ }
1526
+ }
1527
+ }
1528
+ },
1529
+ "responses": {
1530
+ "202": {
1531
+ "description": "Code queued (or silently not, for an unknown address).",
1532
+ "content": {
1533
+ "application/json": {
1534
+ "schema": {
1535
+ "$ref": "#/components/schemas/ApiResponse_PortalLoginStartResult"
1536
+ }
1537
+ }
1538
+ }
1539
+ },
1540
+ "default": {
1541
+ "$ref": "#/components/responses/ApiError"
1542
+ }
1543
+ }
1544
+ }
1545
+ },
1546
+ "/api/v1/portal/login/verify": {
1547
+ "post": {
1548
+ "tags": [
1549
+ "Portal"
1550
+ ],
1551
+ "operationId": "verifyPortalLogin",
1552
+ "summary": "Exchange the e-mailed code for a session",
1553
+ "description": "A wrong, burned or expired code all answer `401 PORTAL_CODE_INVALID` — the three are not distinguished, so the response is not an oracle for which codes exist. The returned `session_token` is a bearer credential for ONE contact: keep it in an HttpOnly cookie on the site's server and never hand it to the browser. Not idempotency-keyed. Errors: `400 VALIDATION_ERROR`, `401 PORTAL_CODE_INVALID`, `403 FORBIDDEN`, `429 RATE_LIMITED`.",
1554
+ "requestBody": {
1555
+ "required": true,
1556
+ "content": {
1557
+ "application/json": {
1558
+ "schema": {
1559
+ "$ref": "#/components/schemas/PortalVerifyInput"
1560
+ }
1561
+ }
1562
+ }
1563
+ },
1564
+ "responses": {
1565
+ "200": {
1566
+ "description": "The new session.",
1567
+ "content": {
1568
+ "application/json": {
1569
+ "schema": {
1570
+ "$ref": "#/components/schemas/ApiResponse_PortalSession"
1571
+ }
1572
+ }
1573
+ }
1574
+ },
1575
+ "default": {
1576
+ "$ref": "#/components/responses/ApiError"
1577
+ }
1578
+ }
1579
+ }
1580
+ },
1581
+ "/api/v1/portal/logout": {
1582
+ "parameters": [
1583
+ {
1584
+ "$ref": "#/components/parameters/PortalSession"
1585
+ }
1586
+ ],
1587
+ "post": {
1588
+ "tags": [
1589
+ "Portal"
1590
+ ],
1591
+ "operationId": "logoutPortal",
1592
+ "summary": "Revoke the session",
1593
+ "description": "Revoking twice reaches the same state — the second call answers `401 PORTAL_SESSION_INVALID`. Not idempotency-keyed. Errors: `401 PORTAL_SESSION_REQUIRED` (header missing), `401 PORTAL_SESSION_INVALID` (unknown, expired or revoked), `403 FORBIDDEN`, `429 RATE_LIMITED`.",
1594
+ "responses": {
1595
+ "204": {
1596
+ "description": "Session revoked."
1597
+ },
1598
+ "default": {
1599
+ "$ref": "#/components/responses/ApiError"
1600
+ }
1601
+ }
1602
+ }
1603
+ },
1604
+ "/api/v1/portal/me": {
1605
+ "parameters": [
1606
+ {
1607
+ "$ref": "#/components/parameters/PortalSession"
1608
+ }
1609
+ ],
1610
+ "get": {
1611
+ "tags": [
1612
+ "Portal"
1613
+ ],
1614
+ "operationId": "getPortalProfile",
1615
+ "summary": "The signed-in contact's profile",
1616
+ "description": "Errors: `401 PORTAL_SESSION_REQUIRED`, `401 PORTAL_SESSION_INVALID`, `403 FORBIDDEN` (key lacks `read:portal`), `429 RATE_LIMITED`.",
1617
+ "responses": {
1618
+ "200": {
1619
+ "description": "Profile.",
1620
+ "content": {
1621
+ "application/json": {
1622
+ "schema": {
1623
+ "$ref": "#/components/schemas/ApiResponse_PortalProfile"
1624
+ }
1625
+ }
1626
+ }
1627
+ },
1628
+ "default": {
1629
+ "$ref": "#/components/responses/ApiError"
1630
+ }
1631
+ }
1632
+ },
1633
+ "patch": {
1634
+ "tags": [
1635
+ "Portal"
1636
+ ],
1637
+ "operationId": "updatePortalProfile",
1638
+ "summary": "Update the signed-in contact's profile",
1639
+ "description": "Only the supplied fields change. `phone: null` clears the number, `family` replaces the whole list, and `marketing_consent` records a `marketing_email` consent decision with source `portal`. Answers the profile as it is after the change. Errors: `400 VALIDATION_ERROR`, `401 PORTAL_SESSION_REQUIRED`, `401 PORTAL_SESSION_INVALID`, `403 FORBIDDEN` (key lacks `write:portal`), `429 RATE_LIMITED`.",
1640
+ "requestBody": {
1641
+ "required": true,
1642
+ "content": {
1643
+ "application/json": {
1644
+ "schema": {
1645
+ "$ref": "#/components/schemas/PortalProfilePatch"
1646
+ }
1647
+ }
1648
+ }
1649
+ },
1650
+ "responses": {
1651
+ "200": {
1652
+ "description": "Updated profile.",
1653
+ "content": {
1654
+ "application/json": {
1655
+ "schema": {
1656
+ "$ref": "#/components/schemas/ApiResponse_PortalProfile"
1657
+ }
1658
+ }
1659
+ }
1660
+ },
1661
+ "default": {
1662
+ "$ref": "#/components/responses/ApiError"
1663
+ }
1664
+ }
1665
+ }
1666
+ },
1667
+ "/api/v1/portal/me/bookings": {
1668
+ "parameters": [
1669
+ {
1670
+ "$ref": "#/components/parameters/PortalSession"
1671
+ }
1672
+ ],
1673
+ "get": {
1674
+ "tags": [
1675
+ "Portal"
1676
+ ],
1677
+ "operationId": "listPortalBookings",
1678
+ "summary": "The signed-in contact's bookings",
1679
+ "description": "Split into `upcoming` and `past`. An upcoming booking still inside the workspace's policy windows carries `manage_token` and `can_manage: true`; the token opens the site's manage page. Errors: `401 PORTAL_SESSION_REQUIRED`, `401 PORTAL_SESSION_INVALID`, `403 FORBIDDEN`, `429 RATE_LIMITED`.",
1680
+ "responses": {
1681
+ "200": {
1682
+ "description": "Bookings.",
1683
+ "content": {
1684
+ "application/json": {
1685
+ "schema": {
1686
+ "$ref": "#/components/schemas/ApiResponse_PortalBookings"
1687
+ }
1688
+ }
1689
+ }
1690
+ },
1691
+ "default": {
1692
+ "$ref": "#/components/responses/ApiError"
1693
+ }
1694
+ }
1695
+ }
1696
+ },
1697
+ "/api/v1/portal/me/export": {
1698
+ "parameters": [
1699
+ {
1700
+ "$ref": "#/components/parameters/PortalSession"
1701
+ }
1702
+ ],
1703
+ "post": {
1704
+ "tags": [
1705
+ "Portal"
1706
+ ],
1707
+ "operationId": "exportPortalData",
1708
+ "summary": "Export everything held about the signed-in contact",
1709
+ "description": "A synchronous GDPR Art. 15 export of the contact's profile, family, consents and bookings, as one JSON document. Read-only, so not idempotency-keyed. Errors: `401 PORTAL_SESSION_REQUIRED`, `401 PORTAL_SESSION_INVALID`, `403 FORBIDDEN`, `429 RATE_LIMITED`.",
1710
+ "responses": {
1711
+ "200": {
1712
+ "description": "The export.",
1713
+ "content": {
1714
+ "application/json": {
1715
+ "schema": {
1716
+ "$ref": "#/components/schemas/ApiResponse_PortalExport"
1717
+ }
1718
+ }
1719
+ }
1720
+ },
1721
+ "default": {
1722
+ "$ref": "#/components/responses/ApiError"
1723
+ }
1724
+ }
1725
+ }
1726
+ },
1727
+ "/api/v1/portal/me/delete": {
1728
+ "parameters": [
1729
+ {
1730
+ "$ref": "#/components/parameters/PortalSession"
1731
+ }
1732
+ ],
1733
+ "post": {
1734
+ "tags": [
1735
+ "Portal"
1736
+ ],
1737
+ "operationId": "deletePortalAccount",
1738
+ "summary": "Erase the signed-in contact",
1739
+ "description": "GDPR Art. 17 erasure. The session is revoked as part of the deletion, so a retry answers `401 PORTAL_SESSION_INVALID` rather than deleting anything else. Not idempotency-keyed. Errors: `401 PORTAL_SESSION_REQUIRED`, `401 PORTAL_SESSION_INVALID`, `403 FORBIDDEN` (key lacks `write:portal`), `429 RATE_LIMITED`.",
1740
+ "responses": {
1741
+ "204": {
1742
+ "description": "Contact erased and session revoked."
1743
+ },
1744
+ "default": {
1745
+ "$ref": "#/components/responses/ApiError"
1746
+ }
1747
+ }
1748
+ }
1749
+ },
1507
1750
  "/api/v1/gdpr/export": {
1508
1751
  "post": {
1509
1752
  "tags": [
@@ -2502,6 +2745,15 @@
2502
2745
  "type": "string"
2503
2746
  }
2504
2747
  },
2748
+ "PortalSession": {
2749
+ "name": "X-Portal-Session",
2750
+ "in": "header",
2751
+ "required": true,
2752
+ "description": "The `session_token` returned by `verifyPortalLogin`. A bearer credential for ONE contact — the site's server keeps it in an HttpOnly cookie and forwards it here. Missing answers `401 PORTAL_SESSION_REQUIRED`; unknown, expired or revoked answers `401 PORTAL_SESSION_INVALID`.",
2753
+ "schema": {
2754
+ "type": "string"
2755
+ }
2756
+ },
2505
2757
  "Cursor": {
2506
2758
  "name": "cursor",
2507
2759
  "in": "query",
@@ -6201,6 +6453,21 @@
6201
6453
  "ApiResponse_ManageSummary": {
6202
6454
  "$ref": "#/components/schemas/Envelope_ManageSummary"
6203
6455
  },
6456
+ "ApiResponse_PortalLoginStartResult": {
6457
+ "$ref": "#/components/schemas/Envelope_PortalLoginStartResult"
6458
+ },
6459
+ "ApiResponse_PortalSession": {
6460
+ "$ref": "#/components/schemas/Envelope_PortalSession"
6461
+ },
6462
+ "ApiResponse_PortalProfile": {
6463
+ "$ref": "#/components/schemas/Envelope_PortalProfile"
6464
+ },
6465
+ "ApiResponse_PortalBookings": {
6466
+ "$ref": "#/components/schemas/Envelope_PortalBookings"
6467
+ },
6468
+ "ApiResponse_PortalExport": {
6469
+ "$ref": "#/components/schemas/Envelope_PortalExport"
6470
+ },
6204
6471
  "ApiResponse_GdprExportRequest": {
6205
6472
  "$ref": "#/components/schemas/Envelope_GdprExportRequest"
6206
6473
  },
@@ -6777,6 +7044,425 @@
6777
7044
  }
6778
7045
  }
6779
7046
  },
7047
+ "PortalLoginStartInput": {
7048
+ "type": "object",
7049
+ "required": [
7050
+ "email"
7051
+ ],
7052
+ "properties": {
7053
+ "email": {
7054
+ "type": "string",
7055
+ "format": "email",
7056
+ "description": "The address the code is sent to."
7057
+ },
7058
+ "locale": {
7059
+ "type": "string",
7060
+ "description": "Locale for the e-mail (e.g. `nb`, `en`); the workspace default when omitted."
7061
+ }
7062
+ }
7063
+ },
7064
+ "PortalLoginStartResult": {
7065
+ "description": "Always `sent`, whether or not the address is a known contact — the route is enumeration-safe by design.",
7066
+ "type": "object",
7067
+ "required": [
7068
+ "status"
7069
+ ],
7070
+ "properties": {
7071
+ "status": {
7072
+ "type": "string",
7073
+ "const": "sent"
7074
+ }
7075
+ }
7076
+ },
7077
+ "PortalVerifyInput": {
7078
+ "type": "object",
7079
+ "required": [
7080
+ "email",
7081
+ "code"
7082
+ ],
7083
+ "properties": {
7084
+ "email": {
7085
+ "type": "string",
7086
+ "format": "email"
7087
+ },
7088
+ "code": {
7089
+ "type": "string",
7090
+ "description": "The one-time code from the e-mail."
7091
+ }
7092
+ }
7093
+ },
7094
+ "PortalContactSummary": {
7095
+ "type": "object",
7096
+ "required": [
7097
+ "contact_id",
7098
+ "first_name"
7099
+ ],
7100
+ "properties": {
7101
+ "contact_id": {
7102
+ "type": "string"
7103
+ },
7104
+ "first_name": {
7105
+ "type": [
7106
+ "string",
7107
+ "null"
7108
+ ]
7109
+ }
7110
+ }
7111
+ },
7112
+ "PortalSession": {
7113
+ "description": "A portal session. `session_token` is a bearer credential for ONE contact — keep it in an HttpOnly cookie on the site's server and never hand it to the browser.",
7114
+ "type": "object",
7115
+ "required": [
7116
+ "session_token",
7117
+ "expires_at",
7118
+ "contact"
7119
+ ],
7120
+ "properties": {
7121
+ "session_token": {
7122
+ "type": "string"
7123
+ },
7124
+ "expires_at": {
7125
+ "type": "integer",
7126
+ "description": "Unix timestamp in milliseconds."
7127
+ },
7128
+ "contact": {
7129
+ "$ref": "#/components/schemas/PortalContactSummary"
7130
+ }
7131
+ }
7132
+ },
7133
+ "PortalFamilyMember": {
7134
+ "type": "object",
7135
+ "required": [
7136
+ "name",
7137
+ "birth_year"
7138
+ ],
7139
+ "properties": {
7140
+ "name": {
7141
+ "type": "string"
7142
+ },
7143
+ "birth_year": {
7144
+ "type": "integer"
7145
+ }
7146
+ }
7147
+ },
7148
+ "PortalProfile": {
7149
+ "type": "object",
7150
+ "required": [
7151
+ "contact_id",
7152
+ "email",
7153
+ "first_name",
7154
+ "last_name",
7155
+ "phone",
7156
+ "family",
7157
+ "marketing_consent",
7158
+ "created_at"
7159
+ ],
7160
+ "properties": {
7161
+ "contact_id": {
7162
+ "type": "string"
7163
+ },
7164
+ "email": {
7165
+ "type": "string"
7166
+ },
7167
+ "first_name": {
7168
+ "type": [
7169
+ "string",
7170
+ "null"
7171
+ ]
7172
+ },
7173
+ "last_name": {
7174
+ "type": [
7175
+ "string",
7176
+ "null"
7177
+ ]
7178
+ },
7179
+ "phone": {
7180
+ "type": [
7181
+ "string",
7182
+ "null"
7183
+ ]
7184
+ },
7185
+ "family": {
7186
+ "type": "array",
7187
+ "items": {
7188
+ "$ref": "#/components/schemas/PortalFamilyMember"
7189
+ }
7190
+ },
7191
+ "marketing_consent": {
7192
+ "type": "boolean"
7193
+ },
7194
+ "created_at": {
7195
+ "type": "integer",
7196
+ "description": "Unix timestamp in milliseconds."
7197
+ }
7198
+ }
7199
+ },
7200
+ "PortalProfilePatch": {
7201
+ "description": "Only the supplied fields change.",
7202
+ "type": "object",
7203
+ "properties": {
7204
+ "first_name": {
7205
+ "type": "string"
7206
+ },
7207
+ "last_name": {
7208
+ "type": "string"
7209
+ },
7210
+ "phone": {
7211
+ "type": [
7212
+ "string",
7213
+ "null"
7214
+ ],
7215
+ "description": "`null` clears the number."
7216
+ },
7217
+ "family": {
7218
+ "type": "array",
7219
+ "description": "Replaces the whole list.",
7220
+ "items": {
7221
+ "$ref": "#/components/schemas/PortalFamilyMember"
7222
+ }
7223
+ },
7224
+ "marketing_consent": {
7225
+ "type": "boolean",
7226
+ "description": "Records a `marketing_email` consent change with source `portal`."
7227
+ }
7228
+ }
7229
+ },
7230
+ "PortalBooking": {
7231
+ "type": "object",
7232
+ "required": [
7233
+ "booking_id",
7234
+ "status",
7235
+ "start_ts",
7236
+ "end_ts",
7237
+ "service_id",
7238
+ "service_name",
7239
+ "resource_id",
7240
+ "resource_name",
7241
+ "booked_for_name",
7242
+ "amount_ore",
7243
+ "notes",
7244
+ "manage_token",
7245
+ "can_manage"
7246
+ ],
7247
+ "properties": {
7248
+ "booking_id": {
7249
+ "type": "string"
7250
+ },
7251
+ "status": {
7252
+ "$ref": "#/components/schemas/BookingStatus"
7253
+ },
7254
+ "start_ts": {
7255
+ "type": "integer",
7256
+ "description": "Unix timestamp in milliseconds."
7257
+ },
7258
+ "end_ts": {
7259
+ "type": "integer",
7260
+ "description": "Unix timestamp in milliseconds."
7261
+ },
7262
+ "service_id": {
7263
+ "type": [
7264
+ "string",
7265
+ "null"
7266
+ ]
7267
+ },
7268
+ "service_name": {
7269
+ "type": [
7270
+ "string",
7271
+ "null"
7272
+ ]
7273
+ },
7274
+ "resource_id": {
7275
+ "type": [
7276
+ "string",
7277
+ "null"
7278
+ ]
7279
+ },
7280
+ "resource_name": {
7281
+ "type": [
7282
+ "string",
7283
+ "null"
7284
+ ]
7285
+ },
7286
+ "booked_for_name": {
7287
+ "type": [
7288
+ "string",
7289
+ "null"
7290
+ ]
7291
+ },
7292
+ "amount_ore": {
7293
+ "type": [
7294
+ "integer",
7295
+ "null"
7296
+ ],
7297
+ "description": "Integer øre, or `null` when the service has no price."
7298
+ },
7299
+ "notes": {
7300
+ "type": [
7301
+ "string",
7302
+ "null"
7303
+ ]
7304
+ },
7305
+ "manage_token": {
7306
+ "type": [
7307
+ "string",
7308
+ "null"
7309
+ ],
7310
+ "description": "Present only while the booking is upcoming and manageable; opens the site's manage page."
7311
+ },
7312
+ "can_manage": {
7313
+ "type": "boolean"
7314
+ }
7315
+ }
7316
+ },
7317
+ "PortalBookings": {
7318
+ "type": "object",
7319
+ "required": [
7320
+ "upcoming",
7321
+ "past"
7322
+ ],
7323
+ "properties": {
7324
+ "upcoming": {
7325
+ "type": "array",
7326
+ "items": {
7327
+ "$ref": "#/components/schemas/PortalBooking"
7328
+ }
7329
+ },
7330
+ "past": {
7331
+ "type": "array",
7332
+ "items": {
7333
+ "$ref": "#/components/schemas/PortalBooking"
7334
+ }
7335
+ }
7336
+ }
7337
+ },
7338
+ "PortalConsentRecord": {
7339
+ "type": "object",
7340
+ "required": [
7341
+ "consent_type",
7342
+ "granted",
7343
+ "granted_at",
7344
+ "revoked_at",
7345
+ "source"
7346
+ ],
7347
+ "properties": {
7348
+ "consent_type": {
7349
+ "type": "string"
7350
+ },
7351
+ "granted": {
7352
+ "type": "boolean"
7353
+ },
7354
+ "granted_at": {
7355
+ "type": [
7356
+ "integer",
7357
+ "null"
7358
+ ],
7359
+ "description": "Unix timestamp in milliseconds, or `null`."
7360
+ },
7361
+ "revoked_at": {
7362
+ "type": [
7363
+ "integer",
7364
+ "null"
7365
+ ],
7366
+ "description": "Unix timestamp in milliseconds, or `null`."
7367
+ },
7368
+ "source": {
7369
+ "type": "string"
7370
+ }
7371
+ }
7372
+ },
7373
+ "PortalExport": {
7374
+ "description": "Everything the workspace holds about the signed-in contact (GDPR Art. 15).",
7375
+ "type": "object",
7376
+ "required": [
7377
+ "exported_at",
7378
+ "contact",
7379
+ "family",
7380
+ "consents",
7381
+ "bookings"
7382
+ ],
7383
+ "properties": {
7384
+ "exported_at": {
7385
+ "type": "integer",
7386
+ "description": "Unix timestamp in milliseconds."
7387
+ },
7388
+ "contact": {
7389
+ "$ref": "#/components/schemas/PortalProfile"
7390
+ },
7391
+ "family": {
7392
+ "type": "array",
7393
+ "items": {
7394
+ "$ref": "#/components/schemas/PortalFamilyMember"
7395
+ }
7396
+ },
7397
+ "consents": {
7398
+ "type": "array",
7399
+ "items": {
7400
+ "$ref": "#/components/schemas/PortalConsentRecord"
7401
+ }
7402
+ },
7403
+ "bookings": {
7404
+ "type": "array",
7405
+ "items": {
7406
+ "$ref": "#/components/schemas/PortalBooking"
7407
+ }
7408
+ }
7409
+ }
7410
+ },
7411
+ "Envelope_PortalLoginStartResult": {
7412
+ "type": "object",
7413
+ "required": [
7414
+ "data"
7415
+ ],
7416
+ "properties": {
7417
+ "data": {
7418
+ "$ref": "#/components/schemas/PortalLoginStartResult"
7419
+ }
7420
+ }
7421
+ },
7422
+ "Envelope_PortalSession": {
7423
+ "type": "object",
7424
+ "required": [
7425
+ "data"
7426
+ ],
7427
+ "properties": {
7428
+ "data": {
7429
+ "$ref": "#/components/schemas/PortalSession"
7430
+ }
7431
+ }
7432
+ },
7433
+ "Envelope_PortalProfile": {
7434
+ "type": "object",
7435
+ "required": [
7436
+ "data"
7437
+ ],
7438
+ "properties": {
7439
+ "data": {
7440
+ "$ref": "#/components/schemas/PortalProfile"
7441
+ }
7442
+ }
7443
+ },
7444
+ "Envelope_PortalBookings": {
7445
+ "type": "object",
7446
+ "required": [
7447
+ "data"
7448
+ ],
7449
+ "properties": {
7450
+ "data": {
7451
+ "$ref": "#/components/schemas/PortalBookings"
7452
+ }
7453
+ }
7454
+ },
7455
+ "Envelope_PortalExport": {
7456
+ "type": "object",
7457
+ "required": [
7458
+ "data"
7459
+ ],
7460
+ "properties": {
7461
+ "data": {
7462
+ "$ref": "#/components/schemas/PortalExport"
7463
+ }
7464
+ }
7465
+ },
6780
7466
  "ScanCreateInput": {
6781
7467
  "type": "object",
6782
7468
  "description": "Provide exactly one of `url`, `orgnr`, or `name`.",