@whooshbang/contracts 3.1.0 → 3.3.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 (95) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/contract-manifest.json +32 -32
  3. package/dist/generated/artifact-index.d.ts +33 -33
  4. package/dist/generated/artifact-index.js +32 -32
  5. package/dist/generated/validators-standalone.d.ts +582 -491
  6. package/dist/generated/validators-standalone.js +9597 -4634
  7. package/dist/generated/validators-standalone.js.map +1 -1
  8. package/dist/generated/validators.d.ts +8 -1
  9. package/dist/generated/validators.js +8 -1
  10. package/dist/generated/validators.js.map +1 -1
  11. package/dist/generated/whooshbang-v1.d.ts +147 -6
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/input-defaults.d.ts +1 -1
  16. package/dist/input-defaults.js.map +1 -1
  17. package/fixtures/core-v1/fixture-index.json +1 -1
  18. package/fixtures/core-v1/scenarios/api-credential-invalid-indistinguishable.json +1 -1
  19. package/fixtures/core-v1/scenarios/api-credential-one-time-reveal.json +1 -1
  20. package/fixtures/core-v1/scenarios/api-credential-revocation-replay.json +1 -1
  21. package/fixtures/core-v1/scenarios/api-credential-rotation-overlap.json +1 -1
  22. package/fixtures/core-v1/scenarios/cancellation-too-late.json +1 -1
  23. package/fixtures/core-v1/scenarios/cross-organization-project.json +1 -1
  24. package/fixtures/core-v1/scenarios/cross-project-resource.json +1 -1
  25. package/fixtures/core-v1/scenarios/environment-mismatch.json +1 -1
  26. package/fixtures/core-v1/scenarios/idempotency-conflict.json +1 -1
  27. package/fixtures/core-v1/scenarios/idempotency-equivalent-replay.json +1 -1
  28. package/fixtures/core-v1/scenarios/malformed-bearer.json +1 -1
  29. package/fixtures/core-v1/scenarios/metadata-overrun.json +1 -1
  30. package/fixtures/core-v1/scenarios/missing-scope.json +1 -1
  31. package/fixtures/core-v1/scenarios/project-create-equivalent-replay.json +1 -1
  32. package/fixtures/core-v1/scenarios/project-slug-conflict.json +1 -1
  33. package/fixtures/core-v1/scenarios/provider-outcome-unknown.json +1 -1
  34. package/fixtures/core-v1/scenarios/subscription-activation-replay.json +1 -1
  35. package/fixtures/core-v1/scenarios/subscription-authorization-capability-opacity.json +1 -1
  36. package/fixtures/core-v1/scenarios/subscription-cross-connection-callback-rejected.json +1 -1
  37. package/fixtures/core-v1/scenarios/subscription-expired-replay.json +1 -1
  38. package/fixtures/core-v1/scenarios/subscription-selected-connection-replay.json +1 -1
  39. package/fixtures/interaction-machine-v1/scenarios/ack-conflict.json +1 -1
  40. package/fixtures/interaction-machine-v1/scenarios/ack-cross-machine.json +1 -1
  41. package/fixtures/interaction-machine-v1/scenarios/ack-out-of-order.json +1 -1
  42. package/fixtures/interaction-machine-v1/scenarios/ack-replay.json +1 -1
  43. package/fixtures/interaction-machine-v1/scenarios/ambiguous-free-text.json +1 -1
  44. package/fixtures/interaction-machine-v1/scenarios/callback-token-opacity.json +1 -1
  45. package/fixtures/interaction-machine-v1/scenarios/credential-registration-conflict.json +1 -1
  46. package/fixtures/interaction-machine-v1/scenarios/credential-registration-replay.json +1 -1
  47. package/fixtures/interaction-machine-v1/scenarios/credential-revocation-replay.json +1 -1
  48. package/fixtures/interaction-machine-v1/scenarios/credential-rotation.json +1 -1
  49. package/fixtures/interaction-machine-v1/scenarios/first-credential-activates.json +1 -1
  50. package/fixtures/interaction-machine-v1/scenarios/interaction-duplicate.json +1 -1
  51. package/fixtures/interaction-machine-v1/scenarios/interaction-expired.json +1 -1
  52. package/fixtures/interaction-machine-v1/scenarios/interaction-mismatch.json +1 -1
  53. package/fixtures/interaction-machine-v1/scenarios/interaction-retired.json +1 -1
  54. package/fixtures/interaction-machine-v1/scenarios/interaction-stale.json +1 -1
  55. package/fixtures/interaction-machine-v1/scenarios/machine-admin-denied.json +1 -1
  56. package/fixtures/interaction-machine-v1/scenarios/machine-bound-message-create.json +1 -1
  57. package/fixtures/interaction-machine-v1/scenarios/machine-cancel-denied.json +1 -1
  58. package/fixtures/interaction-machine-v1/scenarios/machine-crash-before-ack.json +1 -1
  59. package/fixtures/interaction-machine-v1/scenarios/machine-notifier-mismatch.json +1 -1
  60. package/fixtures/interaction-machine-v1/scenarios/machine-other-message-read.json +1 -1
  61. package/fixtures/interaction-machine-v1/scenarios/machine-owned-message-read.json +1 -1
  62. package/fixtures/interaction-machine-v1/scenarios/machine-subscriber-mismatch.json +1 -1
  63. package/fixtures/interaction-machine-v1/scenarios/poll-uncommitted-cursor-rejected.json +1 -1
  64. package/fixtures/interaction-machine-v1/scenarios/project-credential-machine-endpoint-denied.json +1 -1
  65. package/fixtures/interaction-machine-v1/scenarios/quarantine-integrity-rejected.json +1 -1
  66. package/fixtures/interaction-machine-v1/scenarios/quarantine-signature-rejected.json +1 -1
  67. package/fixtures/interaction-machine-v1/scenarios/quarantine-valid.json +1 -1
  68. package/fixtures/interaction-machine-v1/scenarios/revoked-machine-credential.json +1 -1
  69. package/fixtures/recipient-experience-v1/scenarios/membership-broadcast-snapshot.json +1 -1
  70. package/fixtures/recipient-experience-v1/scenarios/preferred-binding-resolution.json +1 -1
  71. package/fixtures/recipient-experience-v1/scenarios/recipient-session-security.json +1 -1
  72. package/fixtures/recipient-experience-v1/scenarios/subscriber-initial-groups-transaction.json +1 -1
  73. package/fixtures/recipient-experience-v1/success/project-recipient-experience.json +2 -0
  74. package/openapi/generated/whooshbang-v1.bundle.json +582 -19
  75. package/openapi/whooshbang-v1.yaml +281 -3
  76. package/package.json +1 -1
  77. package/schemas/core-scenario.schema.json +1 -1
  78. package/schemas/events/agent-binding-degraded-v1.schema.json +1 -1
  79. package/schemas/events/interaction-expired-v1.schema.json +1 -1
  80. package/schemas/events/interaction-received-v1.schema.json +1 -1
  81. package/schemas/events/message-delivered-v1.schema.json +1 -1
  82. package/schemas/events/message-expired-v1.schema.json +1 -1
  83. package/schemas/events/message-failed-v1.schema.json +1 -1
  84. package/schemas/events/message-provider-accepted-v1.schema.json +1 -1
  85. package/schemas/events/subscription-activated-v1.schema.json +1 -1
  86. package/schemas/events/subscription-revoked-v1.schema.json +1 -1
  87. package/schemas/events/whooshbang-event-v1.schema.json +1 -1
  88. package/schemas/fixture-index.schema.json +1 -1
  89. package/schemas/interaction-machine-scenario.schema.json +1 -1
  90. package/schemas/interaction-v1.schema.json +1 -1
  91. package/schemas/machine-v1.schema.json +1 -1
  92. package/schemas/provider-capabilities-v1.schema.json +1 -1
  93. package/schemas/webhooks/webhook-delivery-case-v1.schema.json +1 -1
  94. package/schemas/webhooks/webhook-signing-vector-v1.schema.json +1 -1
  95. package/schemas/whooshbang-v1.schema.json +288 -11
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.1",
3
3
  "info": {
4
4
  "title": "WhooshBang API",
5
- "version": "3.1.0",
5
+ "version": "3.3.0",
6
6
  "description": "The V1 contract for human organization/project administration, project-scoped\nsubscription links, and asynchronous text messages. A Clerk session selects\none organization for administration. A project credential selects one organization,\nproject, and `test` or `live` environment for product operations; request\nbodies cannot override either authenticated scope.\n\nA person's OAuth grant selects one organization and carries scopes, but no\nproject. It names the project and environment it acts in through the URL\npath, on the environment-scoped form of each operation; the service checks\nthat path against the granting person's membership on every call. The flat\nforms remain credential-only and are unchanged.\n\nResource identifiers are opaque and are not authorization. Reads outside the\ncredential scope return the same `404` shape as absent resources. Provider\ncredentials, raw Telegram identity, and protected provider responses are\nnever part of this API.\n",
7
7
  "termsOfService": "https://flowxo.com/terms",
8
8
  "contact": {
@@ -52,6 +52,10 @@
52
52
  "name": "Messages",
53
53
  "description": "Asynchronous logical messages and their highest proven outcomes."
54
54
  },
55
+ {
56
+ "name": "Message categories",
57
+ "description": "Project categories and subscriber choices for those categories."
58
+ },
55
59
  {
56
60
  "name": "Machine clients",
57
61
  "description": "Project-administered, destination-bound local-client identities and one-way credential metadata."
@@ -94,6 +98,218 @@
94
98
  }
95
99
  ],
96
100
  "paths": {
101
+ "/v1/projects/{project_id}/message-categories": {
102
+ "parameters": [{ "$ref": "#/components/parameters/ProjectId" }],
103
+ "get": {
104
+ "operationId": "listMessageCategories",
105
+ "summary": "List project message categories",
106
+ "description": "By default, only current categories are listed. Include archived categories when offering a filter over historical messages.",
107
+ "tags": ["Message categories"],
108
+ "security": [
109
+ { "clerkSession": [] },
110
+ { "bearerGrant": [] },
111
+ { "bearerCredential": [] }
112
+ ],
113
+ "x-required-scopes": ["projects:read"],
114
+ "parameters": [
115
+ {
116
+ "in": "query",
117
+ "name": "include_archived",
118
+ "required": false,
119
+ "schema": { "type": "boolean", "default": false }
120
+ }
121
+ ],
122
+ "responses": {
123
+ "200": {
124
+ "description": "Categories in this project.",
125
+ "content": {
126
+ "application/json": {
127
+ "schema": {
128
+ "$ref": "#/components/schemas/MessageCategoryCollection"
129
+ }
130
+ }
131
+ }
132
+ },
133
+ "401": { "$ref": "#/components/responses/Unauthorized" },
134
+ "403": { "$ref": "#/components/responses/Forbidden" },
135
+ "404": { "$ref": "#/components/responses/NotFound" }
136
+ }
137
+ },
138
+ "post": {
139
+ "operationId": "createMessageCategory",
140
+ "summary": "Add a project message category",
141
+ "tags": ["Message categories"],
142
+ "security": [
143
+ { "clerkSession": [] },
144
+ { "bearerGrant": [] },
145
+ { "bearerCredential": [] }
146
+ ],
147
+ "x-required-scopes": ["projects:write"],
148
+ "requestBody": {
149
+ "required": true,
150
+ "content": {
151
+ "application/json": {
152
+ "schema": {
153
+ "$ref": "#/components/schemas/CreateMessageCategoryRequest"
154
+ }
155
+ }
156
+ }
157
+ },
158
+ "responses": {
159
+ "201": {
160
+ "description": "Category added.",
161
+ "content": {
162
+ "application/json": {
163
+ "schema": { "$ref": "#/components/schemas/MessageCategory" }
164
+ }
165
+ }
166
+ },
167
+ "400": { "$ref": "#/components/responses/BadRequest" },
168
+ "401": { "$ref": "#/components/responses/Unauthorized" },
169
+ "403": { "$ref": "#/components/responses/Forbidden" },
170
+ "409": { "$ref": "#/components/responses/Conflict" }
171
+ }
172
+ }
173
+ },
174
+ "/v1/projects/{project_id}/message-categories/{category_key}": {
175
+ "parameters": [
176
+ { "$ref": "#/components/parameters/ProjectId" },
177
+ {
178
+ "name": "category_key",
179
+ "in": "path",
180
+ "required": true,
181
+ "schema": { "$ref": "#/components/schemas/MessageCategoryKey" }
182
+ }
183
+ ],
184
+ "put": {
185
+ "operationId": "updateMessageCategory",
186
+ "summary": "Change a project's message category",
187
+ "tags": ["Message categories"],
188
+ "security": [
189
+ { "clerkSession": [] },
190
+ { "bearerGrant": [] },
191
+ { "bearerCredential": [] }
192
+ ],
193
+ "x-required-scopes": ["projects:write"],
194
+ "requestBody": {
195
+ "required": true,
196
+ "content": {
197
+ "application/json": {
198
+ "schema": {
199
+ "$ref": "#/components/schemas/UpdateMessageCategoryRequest"
200
+ }
201
+ }
202
+ }
203
+ },
204
+ "responses": {
205
+ "200": {
206
+ "description": "Category updated.",
207
+ "content": {
208
+ "application/json": {
209
+ "schema": { "$ref": "#/components/schemas/MessageCategory" }
210
+ }
211
+ }
212
+ },
213
+ "400": { "$ref": "#/components/responses/BadRequest" },
214
+ "401": { "$ref": "#/components/responses/Unauthorized" },
215
+ "403": { "$ref": "#/components/responses/Forbidden" },
216
+ "404": { "$ref": "#/components/responses/NotFound" },
217
+ "409": { "$ref": "#/components/responses/Conflict" }
218
+ }
219
+ },
220
+ "delete": {
221
+ "operationId": "deleteMessageCategory",
222
+ "summary": "Remove an unused message category",
223
+ "tags": ["Message categories"],
224
+ "description": "The default category and a category still used by queued messages cannot be removed.",
225
+ "security": [
226
+ { "clerkSession": [] },
227
+ { "bearerGrant": [] },
228
+ { "bearerCredential": [] }
229
+ ],
230
+ "x-required-scopes": ["projects:write"],
231
+ "responses": {
232
+ "204": { "description": "Category removed." },
233
+ "401": { "$ref": "#/components/responses/Unauthorized" },
234
+ "403": { "$ref": "#/components/responses/Forbidden" },
235
+ "404": { "$ref": "#/components/responses/NotFound" },
236
+ "409": { "$ref": "#/components/responses/Conflict" }
237
+ }
238
+ }
239
+ },
240
+ "/v1/projects/{project_id}/environments/{environment_id}/notification-preferences/categories": {
241
+ "parameters": [
242
+ { "$ref": "#/components/parameters/ProjectId" },
243
+ { "$ref": "#/components/parameters/EnvironmentId" }
244
+ ],
245
+ "get": {
246
+ "operationId": "getOwnCategoryPreferences",
247
+ "summary": "Read my message category switches",
248
+ "tags": ["Message categories"],
249
+ "security": [{ "clerkSession": [] }],
250
+ "x-required-scopes": ["messages:read"],
251
+ "responses": {
252
+ "200": {
253
+ "description": "Category choices for the signed-in person.",
254
+ "content": {
255
+ "application/json": {
256
+ "schema": {
257
+ "$ref": "#/components/schemas/SubscriberCategoryPreferenceCollection"
258
+ }
259
+ }
260
+ }
261
+ },
262
+ "401": { "$ref": "#/components/responses/Unauthorized" },
263
+ "403": { "$ref": "#/components/responses/Forbidden" }
264
+ }
265
+ }
266
+ },
267
+ "/v1/projects/{project_id}/environments/{environment_id}/notification-preferences/categories/{category_key}": {
268
+ "parameters": [
269
+ { "$ref": "#/components/parameters/ProjectId" },
270
+ { "$ref": "#/components/parameters/EnvironmentId" },
271
+ {
272
+ "name": "category_key",
273
+ "in": "path",
274
+ "required": true,
275
+ "schema": { "$ref": "#/components/schemas/MessageCategoryKey" }
276
+ }
277
+ ],
278
+ "put": {
279
+ "operationId": "setOwnCategoryPreference",
280
+ "summary": "Switch one of my message categories on or off",
281
+ "tags": ["Message categories"],
282
+ "security": [{ "clerkSession": [] }],
283
+ "x-required-scopes": ["messages:write"],
284
+ "requestBody": {
285
+ "required": true,
286
+ "content": {
287
+ "application/json": {
288
+ "schema": {
289
+ "$ref": "#/components/schemas/SetSubscriberCategoryPreferenceRequest"
290
+ }
291
+ }
292
+ }
293
+ },
294
+ "responses": {
295
+ "200": {
296
+ "description": "Updated category choice.",
297
+ "content": {
298
+ "application/json": {
299
+ "schema": {
300
+ "$ref": "#/components/schemas/SubscriberCategoryPreference"
301
+ }
302
+ }
303
+ }
304
+ },
305
+ "400": { "$ref": "#/components/responses/BadRequest" },
306
+ "401": { "$ref": "#/components/responses/Unauthorized" },
307
+ "403": { "$ref": "#/components/responses/Forbidden" },
308
+ "404": { "$ref": "#/components/responses/NotFound" },
309
+ "409": { "$ref": "#/components/responses/Conflict" }
310
+ }
311
+ }
312
+ },
97
313
  "/v1/projects/{project_id}/environments/{environment_id}/whoami": {
98
314
  "get": {
99
315
  "operationId": "whoAmI",
@@ -6692,6 +6908,8 @@
6692
6908
  "accent_color": "#4F46E5",
6693
6909
  "color_scheme": "system"
6694
6910
  },
6911
+ "show_service_category_switches": true,
6912
+ "show_marketing_category_switches": true,
6695
6913
  "row_version": 2,
6696
6914
  "updated_at": "2026-08-22T18:00:00Z",
6697
6915
  "diagnostic_id": "diag_n17_project_experience"
@@ -7276,6 +7494,75 @@
7276
7494
  }
7277
7495
  }
7278
7496
  },
7497
+ "/v1/recipient-session/category-preferences": {
7498
+ "get": {
7499
+ "operationId": "getCategoryPreferences",
7500
+ "summary": "Read this subscriber's message categories and switches",
7501
+ "tags": ["Message categories"],
7502
+ "security": [{ "recipientSessionCapability": [] }],
7503
+ "responses": {
7504
+ "200": {
7505
+ "description": "Category choices for this exact subscriber.",
7506
+ "content": {
7507
+ "application/json": {
7508
+ "schema": {
7509
+ "$ref": "#/components/schemas/SubscriberCategoryPreferenceCollection"
7510
+ }
7511
+ }
7512
+ }
7513
+ },
7514
+ "401": {
7515
+ "$ref": "#/components/responses/RecipientCapabilityInvalid"
7516
+ },
7517
+ "403": { "$ref": "#/components/responses/RecipientOriginForbidden" }
7518
+ }
7519
+ }
7520
+ },
7521
+ "/v1/recipient-session/category-preferences/{category_key}": {
7522
+ "parameters": [
7523
+ {
7524
+ "name": "category_key",
7525
+ "in": "path",
7526
+ "required": true,
7527
+ "schema": { "$ref": "#/components/schemas/MessageCategoryKey" }
7528
+ }
7529
+ ],
7530
+ "put": {
7531
+ "operationId": "setCategoryPreference",
7532
+ "summary": "Switch one of this subscriber's message categories on or off",
7533
+ "tags": ["Message categories"],
7534
+ "security": [{ "recipientSessionCapability": [] }],
7535
+ "requestBody": {
7536
+ "required": true,
7537
+ "content": {
7538
+ "application/json": {
7539
+ "schema": {
7540
+ "$ref": "#/components/schemas/SetSubscriberCategoryPreferenceRequest"
7541
+ }
7542
+ }
7543
+ }
7544
+ },
7545
+ "responses": {
7546
+ "200": {
7547
+ "description": "Updated category choice.",
7548
+ "content": {
7549
+ "application/json": {
7550
+ "schema": {
7551
+ "$ref": "#/components/schemas/SubscriberCategoryPreference"
7552
+ }
7553
+ }
7554
+ }
7555
+ },
7556
+ "400": { "$ref": "#/components/responses/BadRequest" },
7557
+ "401": {
7558
+ "$ref": "#/components/responses/RecipientCapabilityInvalid"
7559
+ },
7560
+ "403": { "$ref": "#/components/responses/RecipientOriginForbidden" },
7561
+ "404": { "$ref": "#/components/responses/NotFound" },
7562
+ "409": { "$ref": "#/components/responses/Conflict" }
7563
+ }
7564
+ }
7565
+ },
7279
7566
  "/v1/recipient-session/logo/{asset_id}": {
7280
7567
  "parameters": [
7281
7568
  {
@@ -8663,7 +8950,7 @@
8663
8950
  "get": {
8664
8951
  "operationId": "listMessagesInEnvironment",
8665
8952
  "summary": "List accepted messages with filters",
8666
- "description": "Content-free message history. Filters combine. accepted_after is inclusive; accepted_before is exclusive. Supply after or page, never both. after accepts the opaque next_after continuation; history intentionally has no total.",
8953
+ "description": "Message history with a retained-content snippet. q searches retained text or an exact message or subscriber ID. After content expires, its message ID still finds it. Filters combine before pagination. accepted_after is inclusive; accepted_before is exclusive. Supply after or page, never both. after accepts the opaque next_after continuation; history intentionally has no total.",
8667
8954
  "tags": ["Administration"],
8668
8955
  "security": [
8669
8956
  { "clerkSession": [] },
@@ -8697,6 +8984,13 @@
8697
8984
  "required": false,
8698
8985
  "schema": { "$ref": "#/components/schemas/OpaqueIdentifier" }
8699
8986
  },
8987
+ {
8988
+ "in": "query",
8989
+ "name": "q",
8990
+ "required": false,
8991
+ "description": "Search retained message text or an exact message or subscriber ID. Expired content is found by message ID only.",
8992
+ "schema": { "type": "string", "minLength": 1, "maxLength": 256 }
8993
+ },
8700
8994
  {
8701
8995
  "in": "query",
8702
8996
  "name": "accepted_after",
@@ -8715,6 +9009,20 @@
8715
9009
  "required": false,
8716
9010
  "schema": { "$ref": "#/components/schemas/OpaqueIdentifier" }
8717
9011
  },
9012
+ {
9013
+ "in": "query",
9014
+ "name": "category_key",
9015
+ "required": false,
9016
+ "description": "Match the category key captured when the message was accepted.",
9017
+ "schema": { "$ref": "#/components/schemas/MessageCategoryKey" }
9018
+ },
9019
+ {
9020
+ "in": "query",
9021
+ "name": "no_category",
9022
+ "required": false,
9023
+ "description": "Match messages accepted before categories were recorded. Cannot be true with category_key.",
9024
+ "schema": { "type": "boolean", "default": false }
9025
+ },
8718
9026
  {
8719
9027
  "in": "query",
8720
9028
  "name": "state",
@@ -11692,6 +12000,133 @@
11692
12000
  }
11693
12001
  },
11694
12002
  "schemas": {
12003
+ "MessageCategoryKey": {
12004
+ "type": "string",
12005
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
12006
+ "minLength": 1,
12007
+ "maxLength": 63
12008
+ },
12009
+ "MessageCategory": {
12010
+ "type": "object",
12011
+ "description": "One project-owned category. Its key stays stable when settings change.",
12012
+ "required": [
12013
+ "key",
12014
+ "name",
12015
+ "description",
12016
+ "purpose",
12017
+ "level",
12018
+ "can_turn_off",
12019
+ "is_default"
12020
+ ],
12021
+ "properties": {
12022
+ "key": { "$ref": "#/components/schemas/MessageCategoryKey" },
12023
+ "name": { "type": "string", "minLength": 1, "maxLength": 100 },
12024
+ "description": { "type": "string", "minLength": 1, "maxLength": 160 },
12025
+ "purpose": { "enum": ["service", "marketing"] },
12026
+ "level": { "enum": ["low", "normal", "high", "critical"] },
12027
+ "can_turn_off": { "type": "boolean" },
12028
+ "is_default": { "type": "boolean" },
12029
+ "opted_out_subscriber_count": {
12030
+ "type": "integer",
12031
+ "minimum": 0,
12032
+ "description": "Number of subscribers in this project who currently have this category switched off. Returned when listing a project's categories."
12033
+ },
12034
+ "archived_at": {
12035
+ "$ref": "#/components/schemas/Timestamp",
12036
+ "description": "Present only when this category has been archived. Historical messages retain their acceptance-time category snapshot."
12037
+ }
12038
+ },
12039
+ "additionalProperties": false
12040
+ },
12041
+ "MessageCategoryCollection": {
12042
+ "type": "object",
12043
+ "required": ["items", "total"],
12044
+ "properties": {
12045
+ "items": {
12046
+ "type": "array",
12047
+ "items": { "$ref": "#/components/schemas/MessageCategory" }
12048
+ },
12049
+ "total": { "$ref": "#/components/schemas/ListTotal" }
12050
+ },
12051
+ "additionalProperties": false
12052
+ },
12053
+ "CreateMessageCategoryRequest": {
12054
+ "type": "object",
12055
+ "required": [
12056
+ "key",
12057
+ "name",
12058
+ "description",
12059
+ "purpose",
12060
+ "level",
12061
+ "can_turn_off"
12062
+ ],
12063
+ "properties": {
12064
+ "key": { "$ref": "#/components/schemas/MessageCategoryKey" },
12065
+ "name": { "type": "string", "minLength": 1, "maxLength": 100 },
12066
+ "description": { "type": "string", "minLength": 1, "maxLength": 160 },
12067
+ "purpose": { "enum": ["service", "marketing"] },
12068
+ "level": { "enum": ["low", "normal", "high", "critical"] },
12069
+ "can_turn_off": { "type": "boolean" }
12070
+ },
12071
+ "additionalProperties": false
12072
+ },
12073
+ "UpdateMessageCategoryRequest": {
12074
+ "type": "object",
12075
+ "minProperties": 1,
12076
+ "properties": {
12077
+ "name": { "type": "string", "minLength": 1, "maxLength": 100 },
12078
+ "description": { "type": "string", "minLength": 1, "maxLength": 160 },
12079
+ "purpose": { "enum": ["service", "marketing"] },
12080
+ "level": { "enum": ["low", "normal", "high", "critical"] },
12081
+ "can_turn_off": { "type": "boolean" }
12082
+ },
12083
+ "additionalProperties": false
12084
+ },
12085
+ "SubscriberCategoryPreference": {
12086
+ "type": "object",
12087
+ "required": [
12088
+ "key",
12089
+ "name",
12090
+ "description",
12091
+ "purpose",
12092
+ "level",
12093
+ "can_turn_off",
12094
+ "is_default",
12095
+ "enabled"
12096
+ ],
12097
+ "properties": {
12098
+ "key": { "$ref": "#/components/schemas/MessageCategoryKey" },
12099
+ "name": { "type": "string", "minLength": 1, "maxLength": 100 },
12100
+ "description": { "type": "string", "minLength": 1, "maxLength": 160 },
12101
+ "purpose": { "enum": ["service", "marketing"] },
12102
+ "level": { "enum": ["low", "normal", "high", "critical"] },
12103
+ "can_turn_off": { "type": "boolean" },
12104
+ "is_default": { "type": "boolean" },
12105
+ "enabled": { "type": "boolean" }
12106
+ },
12107
+ "additionalProperties": false
12108
+ },
12109
+ "SubscriberCategoryPreferenceCollection": {
12110
+ "type": "object",
12111
+ "required": ["items", "total"],
12112
+ "properties": {
12113
+ "items": {
12114
+ "type": "array",
12115
+ "maxItems": 100,
12116
+ "items": {
12117
+ "$ref": "#/components/schemas/SubscriberCategoryPreference"
12118
+ }
12119
+ },
12120
+ "total": { "$ref": "#/components/schemas/ListTotal" }
12121
+ },
12122
+ "additionalProperties": false
12123
+ },
12124
+ "SetSubscriberCategoryPreferenceRequest": {
12125
+ "type": "object",
12126
+ "required": ["enabled"],
12127
+ "properties": { "enabled": { "type": "boolean" } },
12128
+ "additionalProperties": false
12129
+ },
11695
12130
  "WhoAmI": {
11696
12131
  "type": "object",
11697
12132
  "description": "Current principal, authorized context, safe channel readiness and deterministic personal me identity. Contains no email or provider destination.",
@@ -11848,6 +12283,10 @@
11848
12283
  "enum": ["service", "marketing"],
11849
12284
  "default": "service",
11850
12285
  "description": "Omitted means service. Marketing refuses service-only bindings before charging and at every delayed dispatch."
12286
+ },
12287
+ "category_key": {
12288
+ "$ref": "#/components/schemas/MessageCategoryKey",
12289
+ "description": "Optional project category. Omitted service sends use Updates; omitted marketing sends use Marketing."
11851
12290
  }
11852
12291
  },
11853
12292
  "additionalProperties": false,
@@ -12052,6 +12491,10 @@
12052
12491
  "environment": { "$ref": "#/components/schemas/Environment" },
12053
12492
  "id": { "$ref": "#/components/schemas/OpaqueIdentifier" },
12054
12493
  "notifier_id": { "$ref": "#/components/schemas/OpaqueIdentifier" },
12494
+ "category": {
12495
+ "$ref": "#/components/schemas/MessageCategorySnapshot",
12496
+ "description": "Category as accepted. Absent on messages accepted before categories existed."
12497
+ },
12055
12498
  "state": { "$ref": "#/components/schemas/MessageState" },
12056
12499
  "subscriber_id": { "$ref": "#/components/schemas/OpaqueIdentifier" },
12057
12500
  "updated_at": { "$ref": "#/components/schemas/Timestamp" },
@@ -16507,6 +16950,10 @@
16507
16950
  "enum": ["service", "marketing"],
16508
16951
  "default": "service",
16509
16952
  "description": "Omitted means service. Marketing refuses service-only bindings before charging and at every delayed dispatch."
16953
+ },
16954
+ "category_key": {
16955
+ "$ref": "#/components/schemas/MessageCategoryKey",
16956
+ "description": "Optional project category. Omitted service sends use Updates; omitted marketing sends use Marketing."
16510
16957
  }
16511
16958
  },
16512
16959
  "allOf": [
@@ -16620,6 +17067,10 @@
16620
17067
  "description": "What to call the subscriber, resolved as on the subscriber's `name`: your display name, otherwise the channel's own name where the privacy setting keeps it. Absent when there is neither."
16621
17068
  },
16622
17069
  "notifier_id": { "$ref": "#/components/schemas/OpaqueIdentifier" },
17070
+ "category": {
17071
+ "$ref": "#/components/schemas/MessageCategorySnapshot",
17072
+ "description": "Category as accepted. Absent on messages accepted before categories existed."
17073
+ },
16623
17074
  "binding": { "$ref": "#/components/schemas/BindingSummary" },
16624
17075
  "binding_resolution": {
16625
17076
  "type": "string",
@@ -16862,7 +17313,6 @@
16862
17313
  "type": "object",
16863
17314
  "required": ["classified_error"],
16864
17315
  "properties": {
16865
- "attempt_count": { "type": "integer", "minimum": 1 },
16866
17316
  "classified_error": {
16867
17317
  "allOf": [
16868
17318
  { "$ref": "#/components/schemas/ClassifiedError" },
@@ -16872,7 +17322,8 @@
16872
17322
  "code": {
16873
17323
  "enum": [
16874
17324
  "provider_terminal",
16875
- "not_consented_for_marketing"
17325
+ "not_consented_for_marketing",
17326
+ "category_turned_off"
16876
17327
  ]
16877
17328
  },
16878
17329
  "retryable": { "const": false }
@@ -16880,7 +17331,32 @@
16880
17331
  }
16881
17332
  ]
16882
17333
  }
16883
- }
17334
+ },
17335
+ "allOf": [
17336
+ {
17337
+ "if": {
17338
+ "type": "object",
17339
+ "properties": {
17340
+ "classified_error": {
17341
+ "type": "object",
17342
+ "properties": {
17343
+ "code": { "const": "provider_terminal" }
17344
+ },
17345
+ "required": ["code"]
17346
+ }
17347
+ },
17348
+ "required": ["classified_error"]
17349
+ },
17350
+ "then": {
17351
+ "properties": {
17352
+ "attempt_count": {
17353
+ "type": "integer",
17354
+ "minimum": 1
17355
+ }
17356
+ }
17357
+ }
17358
+ }
17359
+ ]
16884
17360
  }
16885
17361
  ]
16886
17362
  }
@@ -17383,6 +17859,54 @@
17383
17859
  "code": { "const": "not_consented_for_marketing" },
17384
17860
  "retryable": { "const": false }
17385
17861
  }
17862
+ },
17863
+ {
17864
+ "type": "object",
17865
+ "properties": {
17866
+ "type": {
17867
+ "const": "https://whooshbang.flowxo.com/problems/category_not_found"
17868
+ },
17869
+ "title": { "const": "Message category not found" },
17870
+ "status": { "const": 404 },
17871
+ "code": { "const": "category_not_found" },
17872
+ "retryable": { "const": false }
17873
+ }
17874
+ },
17875
+ {
17876
+ "type": "object",
17877
+ "properties": {
17878
+ "type": {
17879
+ "const": "https://whooshbang.flowxo.com/problems/category_purpose_mismatch"
17880
+ },
17881
+ "title": { "const": "Purpose and category do not match" },
17882
+ "status": { "const": 422 },
17883
+ "code": { "const": "category_purpose_mismatch" },
17884
+ "retryable": { "const": false }
17885
+ }
17886
+ },
17887
+ {
17888
+ "type": "object",
17889
+ "properties": {
17890
+ "type": {
17891
+ "const": "https://whooshbang.flowxo.com/problems/category_turned_off"
17892
+ },
17893
+ "title": { "const": "Message category turned off" },
17894
+ "status": { "const": 422 },
17895
+ "code": { "const": "category_turned_off" },
17896
+ "retryable": { "const": false }
17897
+ }
17898
+ },
17899
+ {
17900
+ "type": "object",
17901
+ "properties": {
17902
+ "type": {
17903
+ "const": "https://whooshbang.flowxo.com/problems/category_conflict"
17904
+ },
17905
+ "title": { "const": "Message category conflict" },
17906
+ "status": { "const": 409 },
17907
+ "code": { "const": "category_conflict" },
17908
+ "retryable": { "const": false }
17909
+ }
17386
17910
  }
17387
17911
  ],
17388
17912
  "additionalProperties": false
@@ -18066,11 +18590,13 @@
18066
18590
  },
18067
18591
  "ProjectRecipientExperience": {
18068
18592
  "type": "object",
18069
- "description": "The project-owned recipient identity, fallback locale, and email answer behavior. Email answers use one click when no setting has been saved.",
18593
+ "description": "The project-owned recipient identity, fallback locale, email answer behavior, and category switch visibility. Email answers use one click when no setting has been saved.",
18070
18594
  "required": [
18071
18595
  "project_id",
18072
18596
  "default_locale",
18073
18597
  "brand",
18598
+ "show_service_category_switches",
18599
+ "show_marketing_category_switches",
18074
18600
  "row_version",
18075
18601
  "updated_at",
18076
18602
  "diagnostic_id"
@@ -18086,6 +18612,14 @@
18086
18612
  "enum": ["one_click", "confirm"],
18087
18613
  "description": "How an email confirm or select button records its bound answer. Defaults to one_click for existing and new projects; free-text answers always use a form."
18088
18614
  },
18615
+ "show_service_category_switches": {
18616
+ "type": "boolean",
18617
+ "description": "Whether subscriber settings show service category switches. A subscriber who already turned a category off still sees its switch. Defaults to true."
18618
+ },
18619
+ "show_marketing_category_switches": {
18620
+ "type": "boolean",
18621
+ "description": "Whether subscriber settings show marketing category switches. A subscriber who already turned a category off still sees its switch. Defaults to true."
18622
+ },
18089
18623
  "row_version": { "type": "integer", "minimum": 1 },
18090
18624
  "updated_at": { "$ref": "#/components/schemas/Timestamp" },
18091
18625
  "diagnostic_id": { "$ref": "#/components/schemas/DiagnosticId" }
@@ -18220,8 +18754,8 @@
18220
18754
  },
18221
18755
  "UpdateProjectRecipientExperienceRequest": {
18222
18756
  "type": "object",
18223
- "description": "Replaces the project-owned recipient brand and fallback locale under the last observed row version. Omission of email_answer_confirmation preserves its current value, or one_click for an unwritten project.",
18224
- "required": ["default_locale", "brand", "row_version"],
18757
+ "description": "Updates supplied project recipient settings under the last observed row version. Omitted fields keep their current values or project defaults.",
18758
+ "required": ["row_version"],
18225
18759
  "properties": {
18226
18760
  "default_locale": {
18227
18761
  "$ref": "#/components/schemas/RecipientLanguage"
@@ -18232,6 +18766,14 @@
18232
18766
  "enum": ["one_click", "confirm"],
18233
18767
  "description": "Optional project-wide email answer behavior. Omission preserves the current value."
18234
18768
  },
18769
+ "show_service_category_switches": {
18770
+ "type": "boolean",
18771
+ "description": "Show service category switches in subscriber settings. A subscriber who already turned a category off still sees its switch. Omit to keep the current value."
18772
+ },
18773
+ "show_marketing_category_switches": {
18774
+ "type": "boolean",
18775
+ "description": "Show marketing category switches in subscriber settings. A subscriber who already turned a category off still sees its switch. Omit to keep the current value."
18776
+ },
18235
18777
  "row_version": { "type": "integer", "minimum": 1 }
18236
18778
  },
18237
18779
  "additionalProperties": false
@@ -19236,6 +19778,10 @@
19236
19778
  "enum": ["service", "marketing"],
19237
19779
  "default": "service",
19238
19780
  "description": "Omitted means service. Marketing refuses service-only bindings before charging and at every delayed dispatch."
19781
+ },
19782
+ "category_key": {
19783
+ "$ref": "#/components/schemas/MessageCategoryKey",
19784
+ "description": "Optional project category. Omitted service sends use Updates; omitted marketing sends use Marketing."
19239
19785
  }
19240
19786
  },
19241
19787
  "additionalProperties": false,
@@ -19308,10 +19854,10 @@
19308
19854
  },
19309
19855
  "additionalProperties": false
19310
19856
  },
19311
- "Channel": {
19312
- "type": "string",
19313
- "enum": ["telegram", "slack", "email"],
19314
- "description": "A provider-neutral delivery channel. Adding a member makes it representable; it does not claim that every deployment has registered an executable adapter."
19857
+ "ListTotal": {
19858
+ "type": "integer",
19859
+ "minimum": 0,
19860
+ "description": "How many records the list holds in total, not only on this page. Counted while the page was read, so a write landing between the count and the page can leave it a record or two out of step with `items`. That is noise on an inspection surface; it never changes which records a page addresses."
19315
19861
  },
19316
19862
  "ProblemCode": {
19317
19863
  "type": "string",
@@ -19348,7 +19894,11 @@
19348
19894
  "provider_terminal",
19349
19895
  "provider_outcome_unknown",
19350
19896
  "internal_error",
19351
- "not_consented_for_marketing"
19897
+ "not_consented_for_marketing",
19898
+ "category_purpose_mismatch",
19899
+ "category_turned_off",
19900
+ "category_conflict",
19901
+ "category_not_found"
19352
19902
  ]
19353
19903
  },
19354
19904
  "ProblemFieldError": {
@@ -19520,10 +20070,10 @@
19520
20070
  }
19521
20071
  ]
19522
20072
  },
19523
- "ListTotal": {
19524
- "type": "integer",
19525
- "minimum": 0,
19526
- "description": "How many records the list holds in total, not only on this page. Counted while the page was read, so a write landing between the count and the page can leave it a record or two out of step with `items`. That is noise on an inspection surface; it never changes which records a page addresses."
20073
+ "Channel": {
20074
+ "type": "string",
20075
+ "enum": ["telegram", "slack", "email"],
20076
+ "description": "A provider-neutral delivery channel. Adding a member makes it representable; it does not claim that every deployment has registered an executable adapter."
19527
20077
  },
19528
20078
  "StructuredTextSection": {
19529
20079
  "type": "object",
@@ -19961,6 +20511,19 @@
19961
20511
  "enum": ["test", "live"],
19962
20512
  "description": "The single environment selected by the authenticated credential."
19963
20513
  },
20514
+ "MessageCategorySnapshot": {
20515
+ "type": "object",
20516
+ "description": "The category settings recorded when the message was accepted.",
20517
+ "required": ["key", "name", "purpose", "level", "can_turn_off"],
20518
+ "properties": {
20519
+ "key": { "$ref": "#/components/schemas/MessageCategoryKey" },
20520
+ "name": { "type": "string", "minLength": 1, "maxLength": 100 },
20521
+ "purpose": { "enum": ["service", "marketing"] },
20522
+ "level": { "enum": ["low", "normal", "high", "critical"] },
20523
+ "can_turn_off": { "type": "boolean" }
20524
+ },
20525
+ "additionalProperties": false
20526
+ },
19964
20527
  "EmailAddress": {
19965
20528
  "type": "string",
19966
20529
  "minLength": 3,
@@ -21563,7 +22126,7 @@
21563
22126
  "provider-capabilities-v1.schema": {
21564
22127
  "$schema": "https://json-schema.org/draft/2020-12/schema",
21565
22128
  "$id": "https://whooshbang.flowxo.com/schemas/provider-capabilities-v1.schema.json",
21566
- "x-contract-version": "3.1.0",
22129
+ "x-contract-version": "3.3.0",
21567
22130
  "title": "ProviderCapabilitiesV1",
21568
22131
  "description": "Provider-neutral, renderer-facing delivery and interaction capabilities with explicit degradation.",
21569
22132
  "type": "object",
@@ -22365,5 +22928,5 @@
22365
22928
  }
22366
22929
  }
22367
22930
  },
22368
- "x-contract-version": "3.1.0"
22931
+ "x-contract-version": "3.3.0"
22369
22932
  }