@epilot/sdk 2.20.78 → 2.20.80

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 (162) hide show
  1. package/definitions/integration-toolkit.json +251 -63
  2. package/definitions/journey-runtime.json +1 -1
  3. package/definitions/journey.json +399 -13
  4. package/definitions/phone-integration-runtime.json +1 -1
  5. package/definitions/phone-integration.json +651 -0
  6. package/dist/apis/access-token.cjs +6 -6
  7. package/dist/apis/access-token.js +1 -1
  8. package/dist/apis/address-suggestions.cjs +6 -6
  9. package/dist/apis/address-suggestions.js +1 -1
  10. package/dist/apis/address.cjs +6 -6
  11. package/dist/apis/address.js +1 -1
  12. package/dist/apis/ai-agents.cjs +6 -6
  13. package/dist/apis/ai-agents.js +1 -1
  14. package/dist/apis/app.cjs +6 -6
  15. package/dist/apis/app.js +1 -1
  16. package/dist/apis/audit-logs.cjs +6 -6
  17. package/dist/apis/audit-logs.js +1 -1
  18. package/dist/apis/automation.cjs +6 -6
  19. package/dist/apis/automation.js +1 -1
  20. package/dist/apis/billing.cjs +6 -6
  21. package/dist/apis/billing.js +1 -1
  22. package/dist/apis/blueprint-manifest.cjs +6 -6
  23. package/dist/apis/blueprint-manifest.js +1 -1
  24. package/dist/apis/calendar.cjs +6 -6
  25. package/dist/apis/calendar.js +1 -1
  26. package/dist/apis/chat.cjs +6 -6
  27. package/dist/apis/chat.js +1 -1
  28. package/dist/apis/configuration-hub.cjs +6 -6
  29. package/dist/apis/configuration-hub.js +1 -1
  30. package/dist/apis/consent.cjs +6 -6
  31. package/dist/apis/consent.js +1 -1
  32. package/dist/apis/customer-portal.cjs +6 -6
  33. package/dist/apis/customer-portal.js +1 -1
  34. package/dist/apis/dashboard.cjs +6 -6
  35. package/dist/apis/dashboard.js +1 -1
  36. package/dist/apis/data-governance.cjs +6 -6
  37. package/dist/apis/data-governance.js +1 -1
  38. package/dist/apis/deduplication.cjs +6 -6
  39. package/dist/apis/deduplication.js +1 -1
  40. package/dist/apis/design.cjs +6 -6
  41. package/dist/apis/design.js +1 -1
  42. package/dist/apis/document.cjs +6 -6
  43. package/dist/apis/document.js +1 -1
  44. package/dist/apis/email-settings.cjs +6 -6
  45. package/dist/apis/email-settings.js +1 -1
  46. package/dist/apis/email-template.cjs +6 -6
  47. package/dist/apis/email-template.js +1 -1
  48. package/dist/apis/entity-mapping.cjs +6 -6
  49. package/dist/apis/entity-mapping.js +1 -1
  50. package/dist/apis/entity.cjs +6 -6
  51. package/dist/apis/entity.js +1 -1
  52. package/dist/apis/environments.cjs +6 -6
  53. package/dist/apis/environments.js +1 -1
  54. package/dist/apis/event-catalog.cjs +6 -6
  55. package/dist/apis/event-catalog.js +1 -1
  56. package/dist/apis/external-values.cjs +6 -6
  57. package/dist/apis/external-values.js +1 -1
  58. package/dist/apis/file.cjs +6 -6
  59. package/dist/apis/file.js +1 -1
  60. package/dist/apis/iban.cjs +6 -6
  61. package/dist/apis/iban.js +1 -1
  62. package/dist/apis/identity.cjs +6 -6
  63. package/dist/apis/identity.js +1 -1
  64. package/dist/apis/integration-toolkit.cjs +14 -10
  65. package/dist/apis/integration-toolkit.d.cts +2 -2
  66. package/dist/apis/integration-toolkit.d.ts +2 -2
  67. package/dist/apis/integration-toolkit.js +9 -5
  68. package/dist/apis/journey.cjs +8 -8
  69. package/dist/apis/journey.d.cts +2 -2
  70. package/dist/apis/journey.d.ts +2 -2
  71. package/dist/apis/journey.js +2 -2
  72. package/dist/apis/kanban.cjs +6 -6
  73. package/dist/apis/kanban.js +1 -1
  74. package/dist/apis/message.cjs +6 -6
  75. package/dist/apis/message.js +1 -1
  76. package/dist/apis/metering.cjs +6 -6
  77. package/dist/apis/metering.js +1 -1
  78. package/dist/apis/notes.cjs +6 -6
  79. package/dist/apis/notes.js +1 -1
  80. package/dist/apis/notification.cjs +6 -6
  81. package/dist/apis/notification.js +1 -1
  82. package/dist/apis/organization.cjs +6 -6
  83. package/dist/apis/organization.js +1 -1
  84. package/dist/apis/partner-directory.cjs +6 -6
  85. package/dist/apis/partner-directory.js +1 -1
  86. package/dist/apis/permissions.cjs +6 -6
  87. package/dist/apis/permissions.js +1 -1
  88. package/dist/apis/phone-integration.cjs +8 -8
  89. package/dist/apis/phone-integration.d.cts +2 -2
  90. package/dist/apis/phone-integration.d.ts +2 -2
  91. package/dist/apis/phone-integration.js +2 -2
  92. package/dist/apis/pricing-tier.cjs +6 -6
  93. package/dist/apis/pricing-tier.js +1 -1
  94. package/dist/apis/pricing.cjs +6 -6
  95. package/dist/apis/pricing.js +1 -1
  96. package/dist/apis/purpose.cjs +6 -6
  97. package/dist/apis/purpose.js +1 -1
  98. package/dist/apis/query.cjs +6 -6
  99. package/dist/apis/query.js +1 -1
  100. package/dist/apis/sandbox.cjs +6 -6
  101. package/dist/apis/sandbox.js +1 -1
  102. package/dist/apis/sharing.cjs +6 -6
  103. package/dist/apis/sharing.js +1 -1
  104. package/dist/apis/snapshot.cjs +6 -6
  105. package/dist/apis/snapshot.js +1 -1
  106. package/dist/apis/submission.cjs +6 -6
  107. package/dist/apis/submission.js +1 -1
  108. package/dist/apis/target.cjs +6 -6
  109. package/dist/apis/target.js +1 -1
  110. package/dist/apis/targeting.cjs +6 -6
  111. package/dist/apis/targeting.js +1 -1
  112. package/dist/apis/template-variables.cjs +6 -6
  113. package/dist/apis/template-variables.js +1 -1
  114. package/dist/apis/user.cjs +6 -6
  115. package/dist/apis/user.js +1 -1
  116. package/dist/apis/validation-rules.cjs +6 -6
  117. package/dist/apis/validation-rules.js +1 -1
  118. package/dist/apis/webhooks.cjs +6 -6
  119. package/dist/apis/webhooks.js +1 -1
  120. package/dist/apis/workflow-definition.cjs +6 -6
  121. package/dist/apis/workflow-definition.js +1 -1
  122. package/dist/apis/workflow.cjs +6 -6
  123. package/dist/apis/workflow.js +1 -1
  124. package/dist/chunk-JFRRQORA.js +14 -0
  125. package/dist/{chunk-ZREXF3XS.js → chunk-KH4BRB6Z.js} +5 -5
  126. package/dist/{chunk-I3ZZES76.js → chunk-NQBHADQX.js} +1 -1
  127. package/dist/{chunk-M5Z7JYJ3.cjs → chunk-OLRBYITL.cjs} +5 -5
  128. package/dist/{chunk-QYHNZLHS.cjs → chunk-PK5IH3WQ.cjs} +1 -1
  129. package/dist/{chunk-BY76NFAX.cjs → chunk-XPQ7LBUE.cjs} +1 -1
  130. package/dist/index.cjs +12 -12
  131. package/dist/index.d.cts +3 -3
  132. package/dist/index.d.ts +3 -3
  133. package/dist/index.js +3 -3
  134. package/dist/integration-toolkit-3XNG4PL5.cjs +7 -0
  135. package/dist/integration-toolkit-VLTS5HKV.js +7 -0
  136. package/dist/{integration-toolkit.d-l5cgwHLa.d.cts → integration-toolkit.d-BGF0LVB7.d.cts} +587 -85
  137. package/dist/{integration-toolkit.d-l5cgwHLa.d.ts → integration-toolkit.d-BGF0LVB7.d.ts} +587 -85
  138. package/dist/journey-LAEXXNCD.js +7 -0
  139. package/dist/journey-RKBZQRPL.cjs +7 -0
  140. package/dist/{journey-runtime-KZUPV4XH.cjs → journey-runtime-2N77HRB6.cjs} +2 -2
  141. package/dist/{journey-runtime-AQH7EG4C.js → journey-runtime-YJ5G4IG2.js} +1 -1
  142. package/dist/{journey.d-CMtM6U3Y.d.cts → journey.d-5GpTnSld.d.cts} +261 -80
  143. package/dist/{journey.d-CMtM6U3Y.d.ts → journey.d-5GpTnSld.d.ts} +261 -80
  144. package/dist/phone-integration-5EHY236F.js +7 -0
  145. package/dist/phone-integration-C567S75X.cjs +7 -0
  146. package/dist/{phone-integration-runtime-DFA6NYNK.cjs → phone-integration-runtime-3QDC5IMK.cjs} +2 -2
  147. package/dist/{phone-integration-runtime-27B4SHA5.js → phone-integration-runtime-JWF52VHF.js} +1 -1
  148. package/dist/phone-integration.d-DJrZ4xkA.d.cts +539 -0
  149. package/dist/phone-integration.d-DJrZ4xkA.d.ts +539 -0
  150. package/docs/integration-toolkit.md +88 -270
  151. package/docs/journey.md +217 -38
  152. package/docs/phone-integration.md +293 -0
  153. package/package.json +1 -1
  154. package/dist/chunk-UFMZ4EV3.js +0 -14
  155. package/dist/integration-toolkit-FI32O7II.cjs +0 -7
  156. package/dist/integration-toolkit-MHULDW3E.js +0 -7
  157. package/dist/journey-JDIVUY7T.cjs +0 -7
  158. package/dist/journey-PBOMOJ5G.js +0 -7
  159. package/dist/phone-integration-JT2UAOGS.cjs +0 -7
  160. package/dist/phone-integration-KORLYUBG.js +0 -7
  161. package/dist/phone-integration.d-ChHA2oGI.d.cts +0 -179
  162. package/dist/phone-integration.d-ChHA2oGI.d.ts +0 -179
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.0.3",
3
3
  "info": {
4
4
  "title": "Integration Toolkit API",
5
- "version": "1.46.0",
5
+ "version": "1.50.0",
6
6
  "description": "API for integrating with external systems in a standardised way."
7
7
  },
8
8
  "tags": [
@@ -49,7 +49,11 @@
49
49
  "/v1/erp/tracking/acknowledgement": {
50
50
  "post": {
51
51
  "operationId": "acknowledgeTracking",
52
- "x-epilot-permissions": [],
52
+ "x-epilot-permissions": [
53
+ {
54
+ "access": "org"
55
+ }
56
+ ],
53
57
  "summary": "acknowledgeTracking",
54
58
  "description": "Acknowledges an ERP tracking record by removing it from the tracking table, requires public authentication",
55
59
  "tags": [
@@ -148,9 +152,13 @@
148
152
  "post": {
149
153
  "deprecated": true,
150
154
  "operationId": "processErpUpdatesEvents",
151
- "x-epilot-permissions": [],
155
+ "x-epilot-permissions": [
156
+ {
157
+ "action": "integration:ingest"
158
+ }
159
+ ],
152
160
  "summary": "processErpUpdatesEvents",
153
- "description": "Handles updates from ERP systems and tracks them appropriately",
161
+ "description": "Handles updates from ERP systems and tracks them appropriately.\n\nRequires the `integration:ingest` grant on the integration the app component is\nmapped to, or on every integration (`integration:*`) for an app component\nwithout an integration mapping; the wildcard grants `integration:*` and `*`\n(the owner role) include it. Without it the request is refused with 403 before\nany event is processed. *Being rolled out:* until a stage enforces it, a missing\ngrant is logged and the request is still processed.\n",
154
162
  "tags": [
155
163
  "erp"
156
164
  ],
@@ -234,6 +242,9 @@
234
242
  "401": {
235
243
  "$ref": "#/components/responses/Unauthorized"
236
244
  },
245
+ "403": {
246
+ "$ref": "#/components/responses/Forbidden"
247
+ },
237
248
  "429": {
238
249
  "$ref": "#/components/responses/ERPUpdatesRetryableResponse"
239
250
  },
@@ -247,9 +258,14 @@
247
258
  "post": {
248
259
  "deprecated": true,
249
260
  "operationId": "processErpUpdatesEventsV2",
250
- "x-epilot-permissions": [],
261
+ "x-epilot-permissions": [
262
+ {
263
+ "action": "integration:ingest",
264
+ "resource": "integration:{integration_id}"
265
+ }
266
+ ],
251
267
  "summary": "processErpUpdatesEventsV2",
252
- "description": "Handles updates from ERP systems using integration_id directly.\nThis is the v2 version that simplifies the API by accepting integration_id\ninstead of app_id and component_id.\n",
268
+ "description": "Handles updates from ERP systems using integration_id directly.\nThis is the v2 version that simplifies the API by accepting integration_id\ninstead of app_id and component_id.\n\nRequires the `integration:ingest` grant on `integration_id`; the wildcard grants\n`integration:*` and `*` (the owner role) include it. Without it the request is\nrefused with 403 before any event is processed. *Being rolled out:* until a\nstage enforces it, a missing grant is logged and the request is still processed.\n",
253
269
  "tags": [
254
270
  "erp"
255
271
  ],
@@ -294,6 +310,9 @@
294
310
  "401": {
295
311
  "$ref": "#/components/responses/Unauthorized"
296
312
  },
313
+ "403": {
314
+ "$ref": "#/components/responses/Forbidden"
315
+ },
297
316
  "404": {
298
317
  "$ref": "#/components/responses/NotFound"
299
318
  },
@@ -309,9 +328,14 @@
309
328
  "/v3/erp/updates/events": {
310
329
  "post": {
311
330
  "operationId": "processErpUpdatesEventsV3",
312
- "x-epilot-permissions": [],
331
+ "x-epilot-permissions": [
332
+ {
333
+ "action": "integration:ingest",
334
+ "resource": "integration:{integration_id}"
335
+ }
336
+ ],
313
337
  "summary": "processErpUpdatesEventsV3",
314
- "description": "Handles updates from ERP systems using integration_id directly.\nThis is the v3 version that removes the unused event_type field and renames object_type to event_name\nto align with the integration UI naming.\n",
338
+ "description": "Handles updates from ERP systems using integration_id directly.\nThis is the v3 version that removes the unused event_type field and renames object_type to event_name\nto align with the integration UI naming.\n\nRequires the `integration:ingest` grant on `integration_id`; the wildcard grants\n`integration:*` and `*` (the owner role) include it. Without it the request is\nrefused with 403 before anything is processed, charged to the interactive rate\nlimit or recorded — closing a correlation (`correlation_complete`) included.\n*Being rolled out:* until a stage enforces it, a missing grant is logged and the\nrequest is still processed.\n",
315
339
  "tags": [
316
340
  "erp"
317
341
  ],
@@ -354,6 +378,9 @@
354
378
  "401": {
355
379
  "$ref": "#/components/responses/Unauthorized"
356
380
  },
381
+ "403": {
382
+ "$ref": "#/components/responses/Forbidden"
383
+ },
357
384
  "404": {
358
385
  "$ref": "#/components/responses/NotFound"
359
386
  },
@@ -374,7 +401,7 @@
374
401
  }
375
402
  ],
376
403
  "summary": "queryErpUpdateEvents",
377
- "description": "Waits for the events of one correlation — the long poll behind interactive\nregistration — and returns the correlation's summary.\n\nPass the `correlation_id` returned by `POST /v3/erp/updates/events` and say\nwhat you are waiting for:\n\n- `wait_for_entities` — entity slugs, optionally with a selector:\n `contact`, `contract[billing_account_number=4711]`.\n- `wait_for_use_cases` — inbound use case slugs: `contract`, `meter_reading`.\n A meter reading is not an entity, so this is how a wait names one.\n\nThe call returns as soon as `completion` is decided, or after `wait`\nseconds. `completion` is one of:\n\n- `requirements_met` — every listed requirement is satisfied. This wins even\n if an unrelated event of the correlation failed; `errors` still lists it.\n- `complete` — the partner closed the correlation (`correlation_complete`)\n and every event it sent has finished. Nothing further can arrive, so the\n wait ends even if a requirement is unmet. A correlation closed with zero\n events is `complete` at once: the partner sent nothing and never will.\n- `error` — a requirement can no longer be met because an event it depends\n on failed: every event of a required use case ended in error, or an\n entity requirement is still missing when the correlation is complete and\n one of its events failed. Without requirements, `error` means the\n correlation completed with failures.\n- `timeout` — the budget ran out first. The body still shows what has\n landed so far; call again to keep waiting.\n\n**Every requirement is met by the first matching event.** A use case\nrequirement is satisfied by the first event of that use case to finish, and\nan entity requirement without a selector by the first entity of that slug;\nneither says anything about the rest. So, unless the partner sends\n`correlation_complete`, `requirements_met` does not mean the partner has\nfinished: more data for the same customer may still be arriving.\n`correlation_complete` is the only signal that nothing further is coming —\nstrongly recommended for every partner, never required. When you know the\nidentifier you are waiting for, an entity requirement with a selector is the\nprecise tool: `contract[billing_account_number=4711]` is satisfied by that\ncontract and by nothing else.\n\nA requirement is only as good as the use case configuration it refers to:\nmodel one inbound use case per partner event type, and give every use case a\nslug. A use case without a slug cannot be named here.\n\nAn unknown correlation, or one with no interactive events, reads as\n`events_received: 0, closed: false` — \"not yet\" and \"never\" look the same,\nwhich is why `correlation_complete` exists.\n\nWaiting holds a connection, so concurrent waits are capped per organization\nand overall. Over the cap the call is refused with 429 and `Retry-After`;\nfall back to calls with `wait=0` (they never need a slot) or retry later.\nCalls that do not have to wait — `wait=0`, or a correlation that is already\ndecided — are never refused.\n\nRequires `integration:view` on the correlation's integration. The organization\nis always the token's own, so no other organization's correlation is ever\nvisible.\n",
404
+ "description": "Waits for the events of one correlation — the long poll behind interactive\nregistration — and returns the correlation's summary.\n\nPass the `correlation_id` returned by `POST /v3/erp/updates/events` and say\nwhat you are waiting for:\n\n- `wait_for_entities` — entity slugs, optionally with a selector:\n `contact`, `contract[billing_account_number=4711]`.\n- `wait_for_use_cases` — inbound use case slugs: `contract`, `meter_reading`.\n A meter reading is not an entity, so this is how a wait names one.\n\nThe call returns as soon as `completion` is decided, or after `wait`\nseconds. `completion` is one of:\n\n- `requirements_met` — every listed requirement is satisfied. This wins even\n if an unrelated event of the correlation failed; `errors` still lists it.\n- `complete` — the partner closed the correlation (`correlation_complete`)\n and every event it sent has finished. Nothing further can arrive, so the\n wait ends even if a requirement is unmet. A correlation closed with zero\n events is `complete` at once: the partner sent nothing and never will.\n **`complete` is final.** From `completed_at` on, the correlation's answer\n is frozen: an event that still arrives under it (a late event) is\n processed and counted in `events_received`, `events_done` and\n `late_events`, and its errors are listed with `after_completion: true`,\n but it adds nothing to `use_cases` or `entities` and never changes\n `completion` — every later read gives the answer the first one got.\n- `error` — a requirement can no longer be met because an event it depends\n on failed: every event of a required use case ended in error, or an\n entity requirement is still missing when the correlation is complete and\n one of its events failed. Without requirements, `error` means the\n correlation completed with failures. An event that ended `partial` counts\n as failed here: a use case requirement is met only by an event that ended\n `done`.\n- `timeout` — the budget ran out first. The body still shows what has\n landed so far; call again to keep waiting.\n\n**When `entities_omitted` is present, an entity requirement can be\nundecided.** `entities` lists at most 200 entity writes; the rest are only\ncounted. An entity requirement that no listed entity satisfies may then still\nhave been written, so it is neither met nor missing: it is named in\n`undecided_entity_requirements`, it never makes `completion` `error`, and the\nwait still ends `complete` once the correlation completes (if every other\nrequirement is met, `complete` rather than `requirements_met`). Look such an\nentity up yourself, e.g. by its unique id through the entity API. Use case\nrequirements are never undecided: their counts are never omitted.\n\n**Every requirement is met by the first matching event.** A use case\nrequirement is satisfied by the first event of that use case to finish, and\nan entity requirement without a selector by the first entity of that slug;\nneither says anything about the rest. So, unless the partner sends\n`correlation_complete`, `requirements_met` does not mean the partner has\nfinished: more data for the same customer may still be arriving.\n`correlation_complete` is the only signal that nothing further is coming —\nstrongly recommended for every partner, never required. When you know the\nidentifier you are waiting for, an entity requirement with a selector is the\nprecise tool: `contract[billing_account_number=4711]` is satisfied by that\ncontract and by nothing else.\n\nAn event fails when nothing will retry what went wrong, including when its\nmapping could not run or ran only in part: a disabled or unconfigured use\ncase, an item without its unique identifiers, a field or relation that could\nnot be mapped. Such an event ends `error` when it wrote nothing and `partial`\nwhen it wrote something, and `errors` says why. A mapping that writes\nnothing on purpose (an `enabled` expression that is false, an empty list)\nends `done`.\n\nA requirement is only as good as the use case configuration it refers to:\nmodel one inbound use case per partner event type, and give every use case a\nslug. A legacy use case without a slug is counted, and can be named, only by\nits use case id, which differs between environments.\n\nAn unknown correlation, or one with no interactive events, reads as\n`events_received: 0, closed: false` — \"not yet\" and \"never\" look the same,\nwhich is why `correlation_complete` exists. Waiting on a correlation before\nanything was sent under it is normal — mint the id, hand it to the partner,\nwait — and the wait picks the correlation up as soon as its first event is\ningested.\n\nA correlation belongs to one integration: the one whose event (or close)\ncreated it. Events and closes of another integration under the same id are\nnot applied to it — see `correlation_complete` and the event results.\n\nWaiting holds a connection, so concurrent waits are capped per organization\nand overall. A wait that starts before anything was sent under its\ncorrelation counts against a separate, smaller per-organization cap instead,\nfor its whole duration, so waits on ids nothing is ever sent under cannot\ncrowd out waits on correlations that exist. Over a cap the call is refused\nwith 429 and `Retry-After`; fall back to calls with `wait=0` (they never need\na slot) or retry later. Calls that do not have to wait — `wait=0`, or a\ncorrelation that is already decided — are never refused.\n\nRequires `integration:view` on the correlation's integration. The organization\nis always the token's own, so no other organization's correlation is ever\nvisible.\n",
378
405
  "tags": [
379
406
  "erp"
380
407
  ],
@@ -407,7 +434,7 @@
407
434
  "required": false,
408
435
  "style": "form",
409
436
  "explode": true,
410
- "description": "Entities to wait for: `slug` or `slug[key=value]`, at most 20.\n\n- Repeat the parameter for several requirements, one per occurrence\n (`wait_for_entities=contact&wait_for_entities=contract`); a repeated\n occurrence is never split, so its value may contain a comma. Given only\n once, the parameter may instead hold a comma-separated list.\n- Inside the brackets the split is on the **first** `=`, and the value runs\n to the **final** `]`, so a value may contain `=` and `]`.\n- The value is percent-decoded once more after that split, on top of the\n normal URL decoding. That is how a comma travels in the comma-separated\n form (`%2C` inside the value, so `%252C` on the wire), and why a literal\n `%` in a value must be sent as `%25` inside the value.\n\nA selector matches only an attribute that is one of the use case's\nconfigured `unique_ids` for that entity — they are all the toolkit records\nper entity. The value is compared the way entity lookup compares unique ids:\nignoring letter case and surrounding whitespace on both sides, so\n`contact[customer_number=ab-1001]` matches a stored ` AB-1001 `. Whitespace\ninside the value stays significant. A selector whose key is not a unique id of that slug in ANY of\nthe organization's inbound use cases can never match, and is refused with a\n400 whose body names the `slug`, the `rejected_key` and the `allowed_keys`.\nA slug that no inbound use case writes is refused the same way. Treat that\n400 as a configuration error, not a reason to retry. Without a selector, any\nentity of the slug satisfies the requirement, which is only safe when the\nbundle cannot contain more than one of that type. A deleted entity never\nsatisfies a requirement.\n",
437
+ "description": "Entities to wait for: `slug` or `slug[key=value]`, at most 20.\n\n- One requirement per occurrence of the parameter: repeat it for several\n (`wait_for_entities=contact&wait_for_entities=contract`). An occurrence\n is never split, so a comma never separates requirements here — only in\n `wait_for_use_cases`. Any other form of the parameter —\n `wait_for_entities[]=`, `wait_for_entities[0]=`,\n `wait_for_entities[x]=` — is a 400.\n- Encode each occurrence once, as any URL query value\n (`encodeURIComponent`, `URLSearchParams`, `curl --data-urlencode`).\n The value is decoded exactly once, so it reaches the selector exactly as\n you held it: `,`, `%`, `=`, `]` and `+` need no escaping of their own.\n `contact[customer_number=50%]` is sent as\n `wait_for_entities=contact%5Bcustomer_number%3D50%25%5D`.\n- Inside the brackets the split is on the **first** `=`, and the value runs\n to the **final** `]`, so a value may contain `=` and `]` — but not\n `],slug[`: an occurrence holding selectors joined by a comma\n (`contact[a=1],contract[b=2]`) is a 400 telling you to repeat the\n parameter.\n- A value longer than 256 UTF-16 code units, not counting surrounding\n whitespace, is a 400: unique ids that long are not recorded, so it could\n never match. The limit counts code units, not characters: an emoji or\n another character outside the Basic Multilingual Plane counts 2.\n- More than 20 occurrences of the parameter is a 400 naming the limit.\n Empty occurrences count towards it; with 20 or fewer, an empty one is\n dropped.\n\nA selector matches only an attribute that is one of the use case's\nconfigured `unique_ids` for that entity — they are all the toolkit records\nper entity, and only those it could record: an entity write records its\nunique ids within a bound of 1,024 bytes, so an id that does not fit (with\nseveral long ids, the ones after the first in key order) is not recorded,\nand a selector on it cannot match. The value is compared after trimming\nsurrounding whitespace and lower-casing, on both sides, with standard\nUnicode lower-casing (JavaScript `trim` and `toLowerCase`), so\n`contact[customer_number=ab-1001]` matches a stored ` AB-1001 `. For ASCII\nids this behaves like entity lookup; letters with special case rules\n(Turkish `İ`, Greek final `ς`) and non-breaking spaces may compare\ndifferently from entity lookup. Whitespace inside the value stays\nsignificant. A selector whose key is not a unique id of that slug in ANY of\nthe organization's inbound use cases can never match, and is refused with a\n400 whose body names the `slug`, the `rejected_key` and the `allowed_keys`.\nA slug that no inbound use case writes is refused the same way. A meter\nreading reports the `meter` and `meter_counter` it resolves or creates, so\n`meter_counter[external_id=…]` names the unique ids of the use case's\n`meter_counter` block. `_id` is never a selector key: an entity's id is in\nthe response, not among its unique ids. Treat that 400 as a configuration\nerror, not a reason to retry. Without a selector, any\nentity of the slug satisfies the requirement, which is only safe when the\nbundle cannot contain more than one of that type. A deleted entity never\nsatisfies a requirement.\n",
411
438
  "schema": {
412
439
  "type": "array",
413
440
  "items": {
@@ -425,7 +452,7 @@
425
452
  "required": false,
426
453
  "style": "form",
427
454
  "explode": true,
428
- "description": "Inbound use case slugs to wait for, at most 20, repeated or comma-separated.\nNo selectors. Each is met when at least one event of that use case in the\ncorrelation has finished (`done`), not merely been received.\n",
455
+ "description": "Inbound use case slugs to wait for, repeated, comma-separated or both — a\nslug never contains a comma. No selectors. Like `wait_for_entities`, only\n`wait_for_use_cases=…` is accepted; a bracketed form\n(`wait_for_use_cases[]=`) is a 400. More than 20 occurrences of the\nparameter is a 400 naming the limit, empty occurrences included, and so is\nmore than 20 slugs once the occurrences are split on commas (empty entries\nare dropped). Each is met when at\nleast one event of that use case in the correlation has finished (`done`),\nnot merely been received. An entry that is not a use case slug (upper case,\na bracket, a space) is refused with a 400 naming it, as passed, in\n`use_case_slug`. A slug that no inbound use case of the\norganization has can never match and is refused the same way. A correlation\nbelongs to one integration and counts only\nits events, so once it does, a slug that none of THAT integration's inbound\nuse cases has is refused the same way. A legacy use case without a slug is named by its use case\nid.\n",
429
456
  "schema": {
430
457
  "type": "array",
431
458
  "items": {
@@ -450,7 +477,7 @@
450
477
  }
451
478
  },
452
479
  "400": {
453
- "description": "The request is invalid: a missing `correlation_id`, `wait` out of range, a\nmalformed requirement, or a `wait_for_entities` requirement that can never\nmatch. For the last, `slug`, `rejected_key` (absent when no inbound use case\nwrites the slug) and `allowed_keys` say what would have worked.\n",
480
+ "description": "The request is invalid: a missing `correlation_id`, `wait` out of range, a\nmalformed requirement, more than 20 occurrences of a requirement parameter\n(or more than 20 use case slugs), or a requirement that can never match.\nFor a `wait_for_entities` requirement, `slug`, `rejected_key` (absent when\nno inbound use case writes the slug) and `allowed_keys` say what would have\nworked; for a `wait_for_use_cases` entry, `use_case_slug` names the entry\nas passed — one that is not a use case slug, or a slug no inbound use case\nhas.\n\nA requirement is refused only against the configuration as read after the\nrequest arrived, so the wait accepts a use case or unique id saved just\nbefore the call at once. Accepting is not processing: processing applies a\nchanged use case configuration — its unique ids, its mapping, enabled or\ndisabled — within up to 5 minutes, and events pushed meanwhile are\nprocessed with the previous configuration. A selector on a unique id added\nin that time is accepted but cannot match those events. The other direction\nis cached: a use case deleted, or a unique id removed, up to about a minute\nbefore the call may still be accepted, and such a requirement can then\nnever be met.\n",
454
481
  "content": {
455
482
  "application/json": {
456
483
  "schema": {
@@ -537,7 +564,7 @@
537
564
  }
538
565
  ],
539
566
  "summary": "listErpUpdateCorrelations",
540
- "description": "Lists the organization's interactive correlations, newest first by the time\ntheir first event was ingested, with the same summary the correlation wait\nreturns.\n\nIt exists because looking a correlation up by id only finds the ones somebody\nalready has an id for. A correlation that is merely stuck emits no monitoring\ncode, so nothing points at it — and that is the case most worth seeing.\n\n**The listing covers the last 7 days** — the lifetime of a correlation record\n— and says so in `horizon_days`. Correlations older than that are not\nreturned, even when a wider range is being displayed.\n\nPass `next_cursor` back as `cursor` for the next page. A page can be shorter\nthan `limit` and still have more behind it, so stop on `next_cursor: null`.\n\nRequires `integration:view` on `integration_id` when it is given, and on every\nintegration of the organization when it is not.\n",
567
+ "description": "Lists the organization's interactive correlations, newest first by the time\ntheir first event was ingested, with the same summary the correlation wait\nreturns.\n\nIt exists because looking a correlation up by id only finds the ones somebody\nalready has an id for. A correlation that is merely stuck emits no monitoring\ncode, so nothing points at it — and that is the case most worth seeing.\n\n**The listing covers the last 7 days** — the lifetime of a correlation record\n— and says so in `horizon_days`. Correlations older than that are not\nreturned, even when a wider range is being displayed.\n\nPass `next_cursor` back as `cursor` for the next page. Only a null\n`next_cursor` ends the listing: a page can be shorter than `limit`, even\nempty, and still carry a cursor — a page filtered to an integration stops\nafter reading a bounded number of rows, whichever integration they belong\nto. A cursor whose position has aged past the 7 days answers an empty last\npage; a cursor this endpoint did not issue is a 400.\n\nRequires `integration:view` on `integration_id` when it is given, and on every\nintegration of the organization when it is not.\n",
541
568
  "tags": [
542
569
  "erp"
543
570
  ],
@@ -546,10 +573,11 @@
546
573
  "name": "integration_id",
547
574
  "in": "query",
548
575
  "required": false,
549
- "description": "Only correlations of this integration.",
576
+ "description": "Only correlations of this integration. Must be a UUID.",
550
577
  "schema": {
551
578
  "type": "string",
552
- "format": "uuid"
579
+ "format": "uuid",
580
+ "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
553
581
  }
554
582
  },
555
583
  {
@@ -567,7 +595,7 @@
567
595
  "name": "cursor",
568
596
  "in": "query",
569
597
  "required": false,
570
- "description": "Opaque cursor from a prior page's `next_cursor`.",
598
+ "description": "Opaque cursor from a prior page's `next_cursor`. Opaque, but not secret:\nit names the last row the page read, which on a page filtered to an\nintegration may be a correlation of another integration of the\norganization — its correlation id and the time its first event was\ningested. Do not put anything in a correlation id you would not want\nreflected back to a caller who can list another integration.\n",
571
599
  "schema": {
572
600
  "type": "string"
573
601
  }
@@ -602,7 +630,11 @@
602
630
  "/v2/erp/updates/mapping_simulation": {
603
631
  "post": {
604
632
  "operationId": "simulateMappingV2",
605
- "x-epilot-permissions": [],
633
+ "x-epilot-permissions": [
634
+ {
635
+ "access": "org"
636
+ }
637
+ ],
606
638
  "summary": "simulateMappingV2",
607
639
  "description": "Test v2.0 mapping configuration by transforming a payload using the provided mapping rules without persisting data.\n\nThis endpoint accepts the same configuration format that is stored in the integration use case resource,\nmaking it easier to test configurations before saving them to a use case.\n\nSee documentation at /docs/MAPPING_V2.md for detailed v2.0 format specification.\n",
608
640
  "tags": [
@@ -856,7 +888,7 @@
856
888
  "$ref": "#/components/responses/Unauthorized"
857
889
  },
858
890
  "422": {
859
- "description": "Unprocessable entity - mapping configuration or payload validation failed",
891
+ "description": "Unprocessable entity - mapping configuration or payload validation failed, or a downstream epilot API\n(pricing, entity, …) rejected what the mapping made us send with a 4xx other than 408/429. The latter\nanswers `code: DOWNSTREAM_REQUEST_REJECTED` and a `message` naming the API, the rejected request\n(`METHOD /path`), its status and its own error code/message.\n",
860
892
  "content": {
861
893
  "application/json": {
862
894
  "schema": {
@@ -874,7 +906,11 @@
874
906
  "/v1/erp/updates/direct_simulation": {
875
907
  "post": {
876
908
  "operationId": "simulateDirect",
877
- "x-epilot-permissions": [],
909
+ "x-epilot-permissions": [
910
+ {
911
+ "access": "org"
912
+ }
913
+ ],
878
914
  "summary": "simulateDirect",
879
915
  "description": "Dry run for direct-mode payloads: validates a `DirectPayload` against a `direct: true`\nuse case configuration and previews the internal entity and meter reading updates the\npipeline would apply — without persisting anything.\n\nUnlike the ingest endpoint, validation does not stop at the first problem: ALL issues\nacross all operations are collected into `errors`, each carrying the index of the\noperation it refers to. When the payload is valid, the response additionally contains\nthe translated update previews plus non-blocking `warnings` (for example unique-id\nfields that are not defined in the target entity schema, or a failed server-side\nunique-id type derivation).\n\nUse this endpoint while designing a direct integration, before sending events to\n`POST /v3/erp/updates/events`. See documentation at /docs/DIRECT_MODE.md.\n",
880
916
  "tags": [
@@ -1036,7 +1072,11 @@
1036
1072
  "/v1/erp/updates/mapping_simulation": {
1037
1073
  "post": {
1038
1074
  "operationId": "simulateMapping",
1039
- "x-epilot-permissions": [],
1075
+ "x-epilot-permissions": [
1076
+ {
1077
+ "access": "org"
1078
+ }
1079
+ ],
1040
1080
  "summary": "simulateMapping",
1041
1081
  "description": "Test mapping configuration by transforming a payload using the provided mapping rules without persisting data.\n\nSupports both v1.0 (object-based) and v2.0 (event-based) mapping formats.\nSee documentation at /docs/MAPPING_V2.md for detailed v2.0 format specification.\n",
1042
1082
  "tags": [
@@ -1352,7 +1392,7 @@
1352
1392
  "$ref": "#/components/responses/Unauthorized"
1353
1393
  },
1354
1394
  "422": {
1355
- "description": "Unprocessable entity - mapping configuration or payload validation failed",
1395
+ "description": "Unprocessable entity - mapping configuration or payload validation failed, or a downstream epilot API\n(pricing, entity, …) rejected what the mapping made us send with a 4xx other than 408/429. The latter\nanswers `code: DOWNSTREAM_REQUEST_REJECTED` and a `message` naming the API, the rejected request\n(`METHOD /path`), its status and its own error code/message.\n",
1356
1396
  "content": {
1357
1397
  "application/json": {
1358
1398
  "schema": {
@@ -2848,7 +2888,11 @@
2848
2888
  "/v2/integrations/{integrationId}/use-cases/{useCaseId}/secure-proxy-whitelist": {
2849
2889
  "get": {
2850
2890
  "operationId": "getSecureProxyWhitelist",
2851
- "x-epilot-permissions": [],
2891
+ "x-epilot-permissions": [
2892
+ {
2893
+ "access": "internal"
2894
+ }
2895
+ ],
2852
2896
  "summary": "getSecureProxyWhitelist",
2853
2897
  "description": "Returns the current allowed_domains, allowed_ips, and vpc_mode for a secure_proxy use case.\nStaff-only — gated by internal-auth issuer AND admin-portal Cognito user pool membership.\nRejects Login-As tokens.\n",
2854
2898
  "tags": [
@@ -2909,7 +2953,11 @@
2909
2953
  },
2910
2954
  "put": {
2911
2955
  "operationId": "updateSecureProxyWhitelist",
2912
- "x-epilot-permissions": [],
2956
+ "x-epilot-permissions": [
2957
+ {
2958
+ "access": "internal"
2959
+ }
2960
+ ],
2913
2961
  "summary": "updateSecureProxyWhitelist",
2914
2962
  "description": "Replaces allowed_domains and/or allowed_ips on a secure_proxy use case.\nAt least one of the two fields is required. Validation mirrors the CLI's\n`validateDomainPatterns` / `validateCidrs`. Writes a USECASE_HISTORY row\nwith the admin user's email as `changed_by`.\n\nUpdate semantics per field:\n - **omitted** — field is not modified; the stored value is preserved.\n - **non-empty array** — the stored value is replaced with the supplied list.\n - **empty array (`[]`)** — the list is cleared (stored as `[]`). This is\n the canonical way to remove all entries. `null` is not accepted.\n\nStaff-only — same auth gates as GET.\n",
2915
2963
  "tags": [
@@ -2982,7 +3030,11 @@
2982
3030
  "/v2/integrations/{integrationId}/use-cases/{useCaseId}/secure-proxy-whitelist/history": {
2983
3031
  "get": {
2984
3032
  "operationId": "listSecureProxyWhitelistHistory",
2985
- "x-epilot-permissions": [],
3033
+ "x-epilot-permissions": [
3034
+ {
3035
+ "access": "internal"
3036
+ }
3037
+ ],
2986
3038
  "summary": "listSecureProxyWhitelistHistory",
2987
3039
  "description": "Returns the most recent USECASE_HISTORY entries for a secure_proxy use case,\nin reverse chronological order (newest first). Each entry includes the\nactor email (`changed_by`), the ISO-8601 timestamp (`history_created_at`),\nthe `change_description` (free-text action), and the full `configuration`\nsnapshot — from which UI-08 computes a before/after diff between consecutive\nentries.\n\nStaff-only — gated by internal-auth issuer AND admin-portal Cognito user pool\nmembership. Rejects Login-As tokens (same auth gate as the GET / PUT\nsecure-proxy-whitelist operations).\n\nThin wrapper over the service-layer `listUseCaseHistory` that powers\n`GET /v1/integrations/{integrationId}/use-cases/{useCaseId}/history` — the\nseparate path exists because the /v1 variant is tenant-gated and admin-portal\ninternal-auth tokens do not carry tenant permissions.\n",
2988
3040
  "tags": [
@@ -3206,6 +3258,7 @@
3206
3258
  },
3207
3259
  "/v1/integrations/{integrationId}/monitoring/inbound-events": {
3208
3260
  "post": {
3261
+ "deprecated": true,
3209
3262
  "operationId": "queryInboundMonitoringEvents",
3210
3263
  "x-epilot-permissions": [
3211
3264
  {
@@ -3214,7 +3267,7 @@
3214
3267
  }
3215
3268
  ],
3216
3269
  "summary": "queryInboundMonitoringEvents",
3217
- "description": "Query inbound monitoring events for a specific integration.\nReturns detailed information about inbound sync events from ERP systems,\nincluding success rates, error breakdowns, and processing metrics.\n",
3270
+ "description": "**Deprecated and no longer implemented.** The `erp_monitoring` table this\nendpoint read has been retired in favour of the unified `erp_monitoring_v2`\ntable. The endpoint is kept only so existing clients do not 404; it always\nresponds `200` with an empty result and never reads any data.\n\nUse `POST /v2/integrations/{integrationId}/monitoring/events` instead,\nfiltered to `use_case_type: inbound`.\n",
3218
3271
  "tags": [
3219
3272
  "monitoring",
3220
3273
  "integrations"
@@ -3265,6 +3318,7 @@
3265
3318
  },
3266
3319
  "/v1/integrations/{integrationId}/monitoring/stats": {
3267
3320
  "post": {
3321
+ "deprecated": true,
3268
3322
  "operationId": "getMonitoringStats",
3269
3323
  "x-epilot-permissions": [
3270
3324
  {
@@ -3273,7 +3327,7 @@
3273
3327
  }
3274
3328
  ],
3275
3329
  "summary": "getMonitoringStats",
3276
- "description": "Get aggregated statistics for both inbound and outbound monitoring events for a specific integration.\nReturns summary metrics for inbound (ERP sync) and outbound (webhook delivery) events,\nincluding success/error counts and optional breakdowns.\n",
3330
+ "description": "**Deprecated and no longer implemented.** This endpoint aggregated the\nretired `erp_monitoring` table and the webhook delivery log; both are\nsuperseded by the unified `erp_monitoring_v2` table. The endpoint is kept\nonly so existing clients do not 404; it always responds `200` with zeroed\ncounters and never reads any data.\n\nUse `POST /v2/integrations/{integrationId}/monitoring/stats` instead.\n",
3277
3331
  "tags": [
3278
3332
  "monitoring",
3279
3333
  "integrations"
@@ -3324,6 +3378,7 @@
3324
3378
  },
3325
3379
  "/v1/integrations/{integrationId}/monitoring/timeseries": {
3326
3380
  "post": {
3381
+ "deprecated": true,
3327
3382
  "operationId": "getMonitoringTimeSeries",
3328
3383
  "x-epilot-permissions": [
3329
3384
  {
@@ -3332,7 +3387,7 @@
3332
3387
  }
3333
3388
  ],
3334
3389
  "summary": "getMonitoringTimeSeries",
3335
- "description": "Get time-series aggregated event counts for monitoring charts.\nReturns pre-bucketed counts at configurable intervals for both inbound and outbound events.\nMaximum of 200 buckets per request. Returns 400 if the time range and interval would exceed this limit.\n",
3390
+ "description": "**Deprecated and no longer implemented.** This endpoint bucketed the\nretired `erp_monitoring` table and the webhook delivery log; both are\nsuperseded by the unified `erp_monitoring_v2` table. The endpoint is kept\nonly so existing clients do not 404; it always responds `200` with an empty\nbucket list and never reads any data.\n\nUse `POST /v2/integrations/{integrationId}/monitoring/time-series` instead.\n",
3336
3391
  "tags": [
3337
3392
  "monitoring",
3338
3393
  "integrations"
@@ -3436,7 +3491,11 @@
3436
3491
  "/v1/integrations/entities/{entityId}/sync-status": {
3437
3492
  "get": {
3438
3493
  "operationId": "getEntitySyncStatus",
3439
- "x-epilot-permissions": [],
3494
+ "x-epilot-permissions": [
3495
+ {
3496
+ "access": "org"
3497
+ }
3498
+ ],
3440
3499
  "summary": "getEntitySyncStatus",
3441
3500
  "description": "Get the inbound ERP sync status of an entity: when each integration last\nsynchronized (checked) the entity against the ERP, and when it last\nactually changed it. `last_synced_at` also advances on no-op checks —\nevents that were received and evaluated but changed nothing — which by\ndesign leave no trace on the entity itself (no activity feed entry, no\n`_updated_at` bump). Use it to tell whether an entity is up to date with\nthe ERP. Returns an empty list for entities no inbound use case has\nprocessed. Org-scoped via the caller's token.\n\nDeprecated alias: `GET /v1/entities/{entityId}/sync-status` is still\nserved (rewritten to this path in the API handler) but will be removed —\nmigrate callers to this path.\n",
3442
3501
  "tags": [
@@ -3979,6 +4038,7 @@
3979
4038
  },
3980
4039
  "/v1/integrations/{integrationId}/monitoring/outbound-events": {
3981
4040
  "post": {
4041
+ "deprecated": true,
3982
4042
  "operationId": "queryOutboundMonitoringEvents",
3983
4043
  "x-epilot-permissions": [
3984
4044
  {
@@ -3987,7 +4047,7 @@
3987
4047
  }
3988
4048
  ],
3989
4049
  "summary": "queryOutboundMonitoringEvents",
3990
- "description": "Query outbound monitoring events for a specific integration.\nReturns detailed information about outbound event deliveries,\nfiltered by event_name (event_catalog_event) linked to the integration's outbound use cases.\n",
4050
+ "description": "**Deprecated and no longer implemented.** This endpoint read the webhook\ndelivery log directly. The endpoint is kept only so existing clients do not\n404; it always responds `200` with an empty result and never reads any data.\n\nUse instead:\n- `POST /v2/integrations/{integrationId}/monitoring/events` filtered to\n `use_case_type: outbound` for the outcome of each delivery (level, code,\n message, and URL, method and HTTP status in `detail`).\n- The Webhooks API (`POST /v2/webhooks/configs/{configId}/events` and\n `GET /v1/webhooks/configs/{configId}/events/{eventId}`) for a delivery's\n full payload and HTTP response, and to replay it.\n",
3991
4051
  "tags": [
3992
4052
  "monitoring",
3993
4053
  "integrations"
@@ -4404,7 +4464,7 @@
4404
4464
  }
4405
4465
  ],
4406
4466
  "summary": "getMonitoringRunCompletionV2",
4407
- "description": "Per-event completion of one or more runs (every monitoring event sharing a\n`correlation_id`), each reduced to \"how many events have reached a final\noutcome, and how many are still going\". Unlike `getMonitoringStatsV2`, which\ncounts rows, this groups `erp_monitoring_v2` by `event_id` first: an event\nretried twice and then processed counts once, as a success.\n\nPer event: it is **settled** once it has a `success` row, a terminal `error`\nrow (a non-retryable failure, retries exhausted, or the dead-letter closure),\nor nothing but informational rows (deduplicated, skipped). A `warning` never\nsettles an event on its own: mapping and relation caveats are written BEFORE\nthe final stage's success row, and a retry marker (`warning` with\n`will_retry: true`) means the queue will deliver it again. Everything else is\n**in flight**. A settled event's outcome is its worst terminal row — `error`\nbeats `warning` beats `success`.\n\nBatched so a history page costs one request: pass every run on the page. A run\nthat has reported nothing yet comes back with `seen: 0`, never omitted.\n\nThe denominator is NOT here: the producer owns it. For an ERP file import it is\n`ErpImportProgress.events_published`; `seen` is how many of those have reported\nanything yet. Freshness is near-real-time (ClickHouse ingestion lag).\n",
4467
+ "description": "Per-event completion of one or more runs (every monitoring event sharing a\n`correlation_id`), each reduced to \"how many events have reached a final\noutcome, and how many are still going\". Unlike `getMonitoringStatsV2`, which\ncounts rows, this groups `erp_monitoring_v2` by `event_id` first: an event\nretried twice and then processed counts once, as a success.\n\nPer event: it is **settled** once it has a `success` row, a terminal `error`\nrow (a non-retryable failure, retries exhausted, or the dead-letter closure),\nor nothing but informational rows (deduplicated, skipped). A `warning` never\nsettles an event on its own: mapping and relation caveats are written BEFORE\nthe final stage's success row, and a retry marker (`warning` with\n`will_retry: true`) means the queue will deliver it again. Everything else is\n**in flight**. A settled event's outcome is its worst terminal row — `error`\nbeats `warning` beats `success`.\n\nFor an ERP file import, an event the mapping split into several writes (one row\nwriting a product and a price, say) settles only once every write has reported a\nfinal result, whatever those results were; until then it is in flight. A write\nlost without any result keeps its event in flight.\n\nBatched so a history page costs one request: pass every run on the page. A run\nthat has reported nothing yet comes back with `seen: 0`, never omitted.\n\nThe denominator is NOT here: the producer owns it. For an ERP file import it is\n`ErpImportProgress.events_published`; `seen` is how many of those have reported\nanything yet. Freshness is near-real-time (ClickHouse ingestion lag).\n",
4408
4468
  "tags": [
4409
4469
  "monitoring",
4410
4470
  "integrations"
@@ -5564,7 +5624,7 @@
5564
5624
  },
5565
5625
  "ErpImportIssue": {
5566
5626
  "type": "object",
5567
- "description": "A problem found during validation, scoped to the file as a whole rather than to\nindividual rows.\n\n`code` is the translation key and the other fields are its parameters — there is\ndeliberately no message to display. Each code appears at most once (once per\n`expected` for `VALUE_FORMAT_INVALID`), with everything it has to say aggregated\ninto that one entry.\n\n`VALUE_FORMAT_INVALID` — the mapping produces values in a format the entity schema\ndoes not store; `expected` says which format, and `columns` names every column\n(or, for a value built by an expression, the attribute) that holds one.\n\n`MAPPING_EXPRESSION_FAILED` (a warning) — a mapping expression failed for some rows,\nincluding `$number()` on a value it cannot convert such as `12,50` or on a blank cell,\nso importing anyway leaves those values out; `attributes` names the attribute it fills.\n\n`FROM_ROWS_EXPRESSION_FAILED` — an expression in a `conditional.from_rows` item failed\nfor some rows. Blocking, unlike a field: the import would skip every entry of that\ntarget built in the same chunk, not one value; `attributes` names the list attribute.\n\n`lines` points at up to five file lines an issue was seen on, as examples.\n\nThe conditional codes describe a file that cannot be written as variants:\n\n- `TIER_ROWS_NOT_GROUPED` — rows of one variant are spread through the file;\n `columns` names what to group by.\n- `TIER_BANDS_CONFLICT` — two rows of one variant claim the same band.\n- `CONDITION_VALUE_MISSING` — a condition column is blank in every row.\n- `VARIANT_VALUE_CONFLICT` — rows of one variant disagree on a value outside\n `from_rows`; the first row's value is what would be written.\n- `ATTRIBUTE_NOT_OVERRIDABLE` — the schema does not let a variant override this\n attribute, so the mapped value would be discarded.\n- `ATTRIBUTE_NOT_IN_SCHEMA` — the schema does not have the attribute at all.\n\nThe rest reject the mapping against the entity schemas, before the file is read, and\nname the thing at fault in `subject`: `IS_CONDITIONAL_NOT_CONSTANT`,\n`SCHEMA_NOT_CONDITIONABLE`, `SCHEMA_NOT_FOUND`, `SCHEMA_DECLARES_NO_CONDITIONS`,\n`GROUPING_KEY_NOT_A_COLUMN`, `GROUPING_KEY_IS_SORT_COLUMN`,\n`CONDITIONAL_TARGET_MODE_UNSUPPORTED` (a conditional target with any `mode` other\nthan `upsert` — a row of such a target addresses one variant-version, while every\nother mode acts on whole entities; removing variants through an import is not\nsupported yet, and to remove a whole entity, map it as a non-conditional target).",
5627
+ "description": "A problem found during validation, scoped to the file as a whole rather than to\nindividual rows.\n\n`code` is the translation key and the other fields are its parameters — there is\ndeliberately no message to display. Each code appears at most once (once per\n`expected` for `VALUE_FORMAT_INVALID`), with everything it has to say aggregated\ninto that one entry.\n\n`VALUE_FORMAT_INVALID` — the mapping produces values in a format the entity schema\ndoes not store; `expected` says which format, and `columns` names every column\n(or, for a value built by an expression, the attribute) that holds one.\n\n`MAPPING_EXPRESSION_FAILED` (a warning) — a mapping expression failed for some rows,\nincluding `$number()` on a value it cannot convert such as `12,50` or on a blank cell,\nso importing anyway leaves those values out; `attributes` names the attribute it fills.\n\n`FROM_ROWS_EXPRESSION_FAILED` — an expression in a `conditional.from_rows` item failed\nfor some rows. Blocking, unlike a field: the import would skip every entry of that\ntarget built in the same chunk, not one value; `attributes` names the list attribute.\n\n`lines` points at up to five file lines an issue was seen on, as examples.\n\nThe conditional codes describe a file that cannot be written as variants:\n\n- `TIER_ROWS_NOT_GROUPED` — rows of one variant are spread through the file;\n `columns` names what to group by.\n- `TIER_BANDS_CONFLICT` — two rows of one variant claim the same band.\n- `CONDITION_VALUE_MISSING` — a condition column is blank in every row.\n- `VARIANT_VALUE_CONFLICT` — rows of one variant disagree on a value outside\n `from_rows`; the first row's value is what would be written.\n- `ATTRIBUTE_NOT_OVERRIDABLE` — the schema does not let a variant override this\n attribute, so the mapped value would be discarded.\n- `ATTRIBUTE_NOT_IN_SCHEMA` — the schema does not have the attribute at all.\n- `VALID_FROM_NOT_MAPPED` — a conditional target maps no `valid_from`, so the variants\n the file writes take effect immediately. One entry for the whole mapping:\n `attributes` holds `valid_from` once per `entity` type.\n\n`UNIQUE_ID_NOT_IN_SCHEMA` (a blocker) — the mapping looks entities up by an attribute\ntheir schema does not have: a target's own `unique_ids`, or those of a relation or\nrelation ref it sets. One entry for the whole mapping: `attributes` names every missing\nattribute, each with the `entity` type it belongs to.\nSuch a lookup never finds anything, so every re-import would create the entities\nagain, and the engine refuses the relations; the file is not read. A looked-up schema\nthat does not exist is reported as `SCHEMA_NOT_FOUND`; one that cannot be read, or is\nnot the org's own, is left unchecked.\n\nThe rest reject the mapping against the entity schemas, before the file is read, and\nname the thing at fault in `subject`: `IS_CONDITIONAL_NOT_CONSTANT`,\n`SCHEMA_NOT_CONDITIONABLE`, `SCHEMA_NOT_FOUND`, `SCHEMA_DECLARES_NO_CONDITIONS`,\n`GROUPING_KEY_NOT_A_COLUMN`, `GROUPING_KEY_IS_SORT_COLUMN`,\n`CONDITIONAL_TARGET_MODE_UNSUPPORTED` (a conditional target with any `mode` other\nthan `upsert` — a row of such a target addresses one variant-version, while every\nother mode acts on whole entities; removing variants through an import is not\nsupported yet, and to remove a whole entity, map it as a non-conditional target).",
5568
5628
  "required": [
5569
5629
  "code",
5570
5630
  "severity"
@@ -5594,9 +5654,11 @@
5594
5654
  "GROUPING_KEY_NOT_A_COLUMN",
5595
5655
  "GROUPING_KEY_IS_SORT_COLUMN",
5596
5656
  "CONDITIONAL_TARGET_MODE_UNSUPPORTED",
5657
+ "UNIQUE_ID_NOT_IN_SCHEMA",
5597
5658
  "VALUE_FORMAT_INVALID",
5598
5659
  "MAPPING_EXPRESSION_FAILED",
5599
- "FROM_ROWS_EXPRESSION_FAILED"
5660
+ "FROM_ROWS_EXPRESSION_FAILED",
5661
+ "VALID_FROM_NOT_MAPPED"
5600
5662
  ]
5601
5663
  },
5602
5664
  "severity": {
@@ -5628,7 +5690,7 @@
5628
5690
  },
5629
5691
  "attributes": {
5630
5692
  "type": "array",
5631
- "description": "The mapping fields this issue is about, where the value is not a file column as it is: an expression builds it, or the expression fails. VALUE_FORMAT_INVALID (beside `columns`, which holds the values taken from a column as they are), MAPPING_EXPRESSION_FAILED and FROM_ROWS_EXPRESSION_FAILED.",
5693
+ "description": "The mapping fields this issue is about, where the value is not a file column as it is: an expression builds it, or the expression fails. VALUE_FORMAT_INVALID (beside `columns`, which holds the values taken from a column as they are), MAPPING_EXPRESSION_FAILED and FROM_ROWS_EXPRESSION_FAILED; on UNIQUE_ID_NOT_IN_SCHEMA, the unique identifiers that are not attributes of their entity type; on VALID_FROM_NOT_MAPPED, `valid_from` for each conditional entity type that does not map it.",
5632
5694
  "items": {
5633
5695
  "type": "object",
5634
5696
  "required": [
@@ -5638,6 +5700,10 @@
5638
5700
  "name": {
5639
5701
  "type": "string",
5640
5702
  "description": "The attribute the mapping writes, like `unit_amount_decimal`."
5703
+ },
5704
+ "entity": {
5705
+ "type": "string",
5706
+ "description": "The entity type the attribute belongs to, where the issue spans several (UNIQUE_ID_NOT_IN_SCHEMA, VALID_FROM_NOT_MAPPED)."
5641
5707
  }
5642
5708
  }
5643
5709
  }
@@ -6508,7 +6574,9 @@
6508
6574
  "group_id": {
6509
6575
  "type": "string",
6510
6576
  "description": "Optional per-event ordering group. Overrides the request-level `group_id` for THIS event; when absent, the event inherits the request-level `group_id` (or the default strict per-integration ordering). Use it when a single batch carries events for unrelated business objects that may be processed in parallel. See the request-level `group_id` for semantics.\n",
6577
+ "minLength": 1,
6511
6578
  "maxLength": 128,
6579
+ "pattern": "^[!-~]+$",
6512
6580
  "example": "customer-42"
6513
6581
  },
6514
6582
  "correlation_id": {
@@ -6539,27 +6607,29 @@
6539
6607
  },
6540
6608
  "correlation_id": {
6541
6609
  "type": "string",
6542
- "description": "Optional ID that identifies the specific request for debugging purposes"
6610
+ "description": "Optional ID that identifies the specific request. Generated when omitted\nor empty, and returned in the response either way.\n\nWhere it can be waited on - the request closes it with\n`correlation_complete`, or an event that asks for `interactive` inherits\nit - it must be 1-255 characters of letters, digits, hyphens and\nunderscores, or the request is a 400. The same rule applies to an\nevent's own `correlation_id` when that event asks for `interactive`.\nIds nobody can wait on are not checked.\n"
6543
6611
  },
6544
6612
  "group_id": {
6545
6613
  "type": "string",
6546
- "description": "Controls ordering and parallelism for this request's events.\n\nBy default, all events for a given `integration_id` are processed\n**strictly in order, one at a time**. For high-volume integrations this\ncan become a throughput bottleneck.\n\nSet `group_id` to opt into **parallel processing**:\n- Events sharing the same `group_id` are processed in the order received.\n- Events with different `group_id` values are processed in parallel.\n\nTypical usage is to derive `group_id` from a logical partition key in\nyour payload — for example the customer ID, contract ID, or meter ID —\nso updates to the same business object remain ordered while unrelated\nobjects are processed concurrently.\n\nNotes:\n- Individual events may set their own `group_id`, which overrides this\n request-level value for that event only.\n- Up to 20 groups per integration are processed concurrently. Using\n more distinct values than that yields no additional parallelism.\n- Omit this field if strict per-integration ordering is required.\n",
6614
+ "description": "Controls ordering and parallelism for this request's events.\n\nBy default, all events for a given `integration_id` are processed\n**strictly in order, one at a time**. For high-volume integrations this\ncan become a throughput bottleneck.\n\nSet `group_id` to opt into **parallel processing**:\n- Events sharing the same `group_id` are processed in the order received.\n- Events with different `group_id` values are processed in parallel.\n\nTypical usage is to derive `group_id` from a logical partition key in\nyour payload — for example the customer ID, contract ID, or meter ID —\nso updates to the same business object remain ordered while unrelated\nobjects are processed concurrently.\n\nNotes:\n- Individual events may set their own `group_id`, which overrides this\n request-level value for that event only.\n- Up to 20 groups per integration are processed concurrently. Using\n more distinct values than that yields no additional parallelism.\n- Omit this field if strict per-integration ordering is required.\n- 1 to 128 printable ASCII characters without spaces: letters, digits and\n punctuation (`!` to `~`). Anything else is a 400.\n",
6615
+ "minLength": 1,
6547
6616
  "maxLength": 128,
6617
+ "pattern": "^[!-~]+$",
6548
6618
  "example": "customer-42"
6549
6619
  },
6550
6620
  "interactive": {
6551
6621
  "type": "boolean",
6552
- "description": "Request interactive processing for this request's events.\n\nInteractive events are carried on a dedicated, low-latency lane and\nrecorded, so a caller can wait for their result with\n`GET /v3/erp/updates/events?correlation_id=...`. Use it for the pushes a\nperson is waiting on — a registration, a self-service lookup — not for\nbatch loads.\n\nThe use case has to allow it: its `interactive` setting must be\n`on_request` (this flag then selects the lane) or `always` (every event of\nthat use case is interactive and this flag is unnecessary). On a use case\nthat does not allow it the events are processed normally and the result\nsays `interactive: false`.\n\nAn organization that asks for interactive processing faster than its agreed\nrate has the excess carried on its normal lane instead. Nothing is rejected\nand nothing is lost: those results still say `interactive: true`, with\n`lane` naming the queue that actually carried them, and the correlation can\nstill be waited on - it will simply take as long as a bulk event does.\n\nIndividual events may set their own `interactive`, which overrides this\nrequest-level value for that event only.\n",
6622
+ "description": "Request interactive processing for this request's events.\n\nInteractive events are carried on a dedicated, low-latency lane and\nrecorded, so a caller can wait for their result with\n`GET /v3/erp/updates/events?correlation_id=...`. Use it for the pushes a\nperson is waiting on — a registration, a self-service lookup — not for\nbatch loads.\n\nThe use case has to allow it: its `interactive` setting must be\n`on_request` (this flag then selects the lane) or `always` (every event of\nthat use case is interactive and this flag is unnecessary). On a use case\nthat does not allow it the events are processed normally and the result\nsays `interactive: false`.\n\nAn organization that asks for interactive processing faster than its agreed\nrate has the excess carried on its normal lane instead. Nothing is rejected\nand nothing is lost: those results still say `interactive: true`, with\n`lane` naming the queue that actually carried them, and the correlation can\nstill be waited on - it will simply take as long as a bulk event does.\nOnly events admitted as interactive count against that rate: an event\nrefused at ingest (422), skipped as a duplicate, or past its correlation's\nevent limit costs nothing. A request is admitted or spilled as a whole.\n\nIndividual events may set their own `interactive`, which overrides this\nrequest-level value for that event only.\n",
6553
6623
  "example": true
6554
6624
  },
6555
6625
  "correlation_complete": {
6556
6626
  "type": "boolean",
6557
- "description": "Declares that no further events will follow for this request's\n`correlation_id`. A caller waiting on that correlation is answered as soon\nas the events already sent have finished, instead of waiting out its full\nbudget.\n\nRequires a request-level `correlation_id` - it closes that correlation and\nis never inferred from per-event `correlation_id` values; sending it\nwithout one is a 400.\n\nIt is valid on a request with an empty `events` array, which is how a\nbridge closes a correlation whose terminal marker arrived on its own. If\nnothing was ever pushed under that correlation id, it is created closed and\nempty, so a waiter is told straight away that nothing is coming rather than\nwaiting for data that will never arrive.\n\nClosing a correlation twice is a no-op. Events that arrive afterwards are\nstill processed, and raise a `CORRELATION_LATE_EVENT` monitoring warning.\n",
6627
+ "description": "Declares that no further events will follow for this request's\n`correlation_id`. A caller waiting on that correlation is answered as soon\nas the events already sent have finished, instead of waiting out its full\nbudget.\n\nRequires a request-level `correlation_id` - it closes that correlation and\nis never inferred from per-event `correlation_id` values; sending it\nwithout one is a 400.\n\nIt is valid on a request with an empty `events` array, which is how a\nbridge closes a correlation whose terminal marker arrived on its own. If\nnothing was ever pushed under that correlation id, it is created closed and\nempty, so a waiter is told straight away that nothing is coming rather than\nwaiting for data that will never arrive.\n\nA request that carries events closes the correlation whenever it\nexists - also when those events came back `skipped` as duplicates of\nevents already sent (a retry of the closing request), or were processed\non the normal lane - but never creates it: only a request with no events\ndoes. A request with no events that creates the correlation costs one unit\nof the organization's interactive rate; closing one that exists, or finding\nit closed already, costs nothing. When the organization is over its rate,\nit still closes a correlation that exists but creates none - send it again\na few seconds later. The close is not applied when the response asks for a retry (422 or\n429), when the queue refused one of the request's events, or when the\ncorrelation does not exist and the request carries events or could not\npay for creating it; each of these raises a `CORRELATION_CLOSE_IGNORED`\nmonitoring warning naming the cause. The response says what became of\nthe close in `correlation_close` - `applied`, `rate_limited` (send it again\na few seconds later) or `ignored` with its `reasons`.\n\nClosing a correlation twice is a no-op. Events that arrive afterwards are\nstill processed, and raise a `CORRELATION_LATE_EVENT` monitoring warning;\nonce the correlation is complete they can no longer change its answer\n(see `completion` on `GET /v3/erp/updates/events`).\n\nA correlation belongs to the integration whose event or close created it.\nA close sent for another integration under the same id is not applied and\nraises `CORRELATION_CLOSE_IGNORED`; an event sent for another integration\nunder it is processed normally, not interactively, and its result says\n`interactive: false` with the reason.\n",
6558
6628
  "example": true
6559
6629
  },
6560
6630
  "events": {
6561
6631
  "type": "array",
6562
- "description": "List of ERP events to process. At least one is required, unless\n`correlation_complete` is `true` - a request that only closes a correlation\nmay send an empty array.\n\nNote: `wait` is NOT accepted on this endpoint and returns 400. Ingest never\nblocks; poll `GET /v3/erp/updates/events` to wait for a correlation.\n",
6632
+ "description": "List of ERP events to process. At least one is required, unless\n`correlation_complete` is `true` - a request that only closes a correlation\nmay send an empty array.\n\nNote: ingest never blocks. A `wait` field, on the request or on an event, is\nignored; poll `GET /v3/erp/updates/events` to wait for a correlation.\n",
6563
6633
  "items": {
6564
6634
  "$ref": "#/components/schemas/ErpEventV3"
6565
6635
  }
@@ -6593,11 +6663,11 @@
6593
6663
  "deleted",
6594
6664
  "unchanged"
6595
6665
  ],
6596
- "description": "What the write did. `unchanged` means the entity already matched the\nevent; it still counts as written.\n"
6666
+ "description": "What the writes did, merged over every write of the entity — by the event,\nor on a correlation by all its events: `deleted` wins, then `created`,\n`updated`, `unchanged`. So an entity the event (or the correlation) created\nreads `created` however often it was written again, and one it deleted\nreads `deleted`. `unchanged` means the entity already matched; it still\ncounts as written.\n"
6597
6667
  },
6598
6668
  "unique_ids": {
6599
6669
  "type": "object",
6600
- "description": "The unique ids the entity was looked up by — exactly the use case's\nconfigured `unique_ids` for that entity and nothing else. These and `_id`\nare what a caller can tell several entities of one slug apart by, and the\nonly attributes a `wait_for_entities` selector can match.\n",
6670
+ "description": "The unique ids the entity was looked up by — the use case's configured\n`unique_ids` for that entity and nothing else, as far as they could be\nrecorded. A value longer than 256 UTF-16 code units, not counting\nsurrounding whitespace, is not recorded, and one write records its unique\nids, in key order, within 1,024 bytes: an id that would not fit is left\nout. A selector cannot name an id that is not recorded. These and `_id`\nare what a caller can tell several entities of one slug apart by, and the\nonly attributes a `wait_for_entities` selector can match. On a correlation\nthey are the union over its events: two use cases that wrote one contact by\ndifferent unique ids both match. A unique id two events wrote with\ndifferent values shows one of them here, and a selector matches either.\n",
6601
6671
  "additionalProperties": {
6602
6672
  "type": "string"
6603
6673
  },
@@ -6607,6 +6677,49 @@
6607
6677
  }
6608
6678
  }
6609
6679
  },
6680
+ "CorrelationCloseResult": {
6681
+ "type": "object",
6682
+ "description": "What became of the request's `correlation_complete`. Present exactly when the\nrequest carried `correlation_complete: true`; absent otherwise.\n",
6683
+ "required": [
6684
+ "outcome"
6685
+ ],
6686
+ "properties": {
6687
+ "outcome": {
6688
+ "type": "string",
6689
+ "enum": [
6690
+ "applied",
6691
+ "rate_limited",
6692
+ "ignored"
6693
+ ],
6694
+ "description": "- `applied` - the correlation is closed: by this request, before it (closing\n twice is a no-op), or created closed and empty by a request without events.\n- `rate_limited` - a request without events found no correlation and the\n organization was over its interactive rate (or the rate could not be\n checked), so the correlation was not created. Send the close again a few\n seconds later; until then a waiter on it is not told that nothing is\n coming.\n- `ignored` - the close was not applied; `reasons` says why and what to do.\n Each such close also raises a `CORRELATION_CLOSE_IGNORED` monitoring\n warning with the same reasons.\n"
6695
+ },
6696
+ "reasons": {
6697
+ "type": "array",
6698
+ "description": "Why the close was not applied, each cause once. Absent when `applied`.\n\n- `retry_requested` - the response is a 422 or 429; the retry carries\n `correlation_complete` again and closes then.\n- `queue_refused` - the queue refused an event of the request; send the\n close again once every event is accepted.\n- `rate_limited`, `rate_limit_unavailable` - see `outcome: rate_limited`.\n- `other_integration` - the correlation belongs to another integration.\n- `events_under_own_correlation_ids`, `events_duplicate`,\n `events_not_configured`, `events_not_interactive`, `events_unrecorded` -\n the correlation does not exist and this request, which carries events,\n recorded none under it, so there was nothing to close.\n- `close_failed` - the close could not be written; send it again.\n- `interactive_unavailable` - interactive processing is not available, so\n there is no correlation to close.\n",
6699
+ "items": {
6700
+ "type": "string",
6701
+ "enum": [
6702
+ "retry_requested",
6703
+ "queue_refused",
6704
+ "events_under_own_correlation_ids",
6705
+ "events_duplicate",
6706
+ "events_not_configured",
6707
+ "events_not_interactive",
6708
+ "events_unrecorded",
6709
+ "rate_limited",
6710
+ "rate_limit_unavailable",
6711
+ "other_integration",
6712
+ "close_failed",
6713
+ "interactive_unavailable"
6714
+ ]
6715
+ }
6716
+ },
6717
+ "message": {
6718
+ "type": "string",
6719
+ "description": "The reasons as text. Absent when `applied`."
6720
+ }
6721
+ }
6722
+ },
6610
6723
  "ErpUpdateProcessingError": {
6611
6724
  "type": "object",
6612
6725
  "description": "A failure nothing will retry.",
@@ -6630,6 +6743,10 @@
6630
6743
  "occurred_at": {
6631
6744
  "type": "string",
6632
6745
  "format": "date-time"
6746
+ },
6747
+ "after_completion": {
6748
+ "type": "boolean",
6749
+ "description": "`true` on a correlation's error of an event that arrived after the\ncorrelation completed. Listed for the record — while the list has room,\nand counted in `errors_total` either way; it is not part of the\ncorrelation's answer, which was final at `completed_at`. Absent otherwise.\n"
6633
6750
  }
6634
6751
  }
6635
6752
  },
@@ -6644,7 +6761,8 @@
6644
6761
  "writes_total",
6645
6762
  "writes_done",
6646
6763
  "entities",
6647
- "errors"
6764
+ "errors",
6765
+ "errors_total"
6648
6766
  ],
6649
6767
  "properties": {
6650
6768
  "event_id": {
@@ -6665,7 +6783,7 @@
6665
6783
  "partial",
6666
6784
  "error"
6667
6785
  ],
6668
- "description": "`accepted` until the mapping ran, `processing` while its writes run, then\none of the terminal states: `done`, `partial` (some writes failed) or\n`error`.\n"
6786
+ "description": "`accepted` until the mapping ran, `processing` while its writes run, then\none of the terminal states: `done`, `partial` (something was written but\na write or part of the mapping failed) or `error` (nothing was written).\n"
6669
6787
  },
6670
6788
  "lane": {
6671
6789
  "type": "string",
@@ -6711,15 +6829,25 @@
6711
6829
  },
6712
6830
  "entities": {
6713
6831
  "type": "array",
6832
+ "description": "The entities the event wrote. An entity written without one of its\nrelations, because the payload lacked the related entity's unique\nidentifiers, is not listed; the event ends `partial` and `errors` names\nthe relation.\n",
6714
6833
  "items": {
6715
6834
  "$ref": "#/components/schemas/ErpUpdateProcessedEntity"
6716
6835
  }
6717
6836
  },
6837
+ "entities_omitted": {
6838
+ "type": "integer",
6839
+ "description": "Entity writes of the event that are not in `entities`: an event lists at\nmost 200. The entities were written all the same; they are only missing\nfrom the list. Present only when there are any.\n"
6840
+ },
6718
6841
  "errors": {
6719
6842
  "type": "array",
6843
+ "description": "The event's first 10 failures. A failure repeated within one item of the\npartner's data is listed once. `errors_total` counts them all.\n",
6720
6844
  "items": {
6721
6845
  "$ref": "#/components/schemas/ErpUpdateProcessingError"
6722
6846
  }
6847
+ },
6848
+ "errors_total": {
6849
+ "type": "integer",
6850
+ "description": "Every failure the event had, including those not listed in `errors`."
6723
6851
  }
6724
6852
  }
6725
6853
  },
@@ -6755,12 +6883,14 @@
6755
6883
  "required": [
6756
6884
  "correlation_id",
6757
6885
  "closed",
6886
+ "complete",
6758
6887
  "events_received",
6759
6888
  "events_done",
6760
6889
  "late_events",
6761
6890
  "use_cases",
6762
6891
  "entities",
6763
- "errors"
6892
+ "errors",
6893
+ "errors_total"
6764
6894
  ],
6765
6895
  "properties": {
6766
6896
  "correlation_id": {
@@ -6777,6 +6907,15 @@
6777
6907
  "type": "string",
6778
6908
  "format": "date-time"
6779
6909
  },
6910
+ "complete": {
6911
+ "type": "boolean",
6912
+ "description": "The correlation is complete — closed, and every event it received before\ncompleting has finished (or it was closed having received none). FINAL once\n`true`: the field to read for \"is it done\", on the listing as on a wait.\nDo not derive it from `events_done >= events_received`: a late event\nraises `events_received` after completion without reopening anything.\n"
6913
+ },
6914
+ "completed_at": {
6915
+ "type": "string",
6916
+ "format": "date-time",
6917
+ "description": "When the correlation became complete. Present exactly when `complete` is\n`true` (except on correlations completed before 2026-10-14); the\ncorrelation's `use_cases`, `entities` and `completion` are frozen at this\ninstant.\n"
6918
+ },
6780
6919
  "events_received": {
6781
6920
  "type": "integer"
6782
6921
  },
@@ -6786,11 +6925,12 @@
6786
6925
  },
6787
6926
  "late_events": {
6788
6927
  "type": "integer",
6789
- "description": "Events that arrived after the correlation was closed."
6928
+ "description": "Events that arrived after the correlation was closed. Those that arrived\nafter it completed are counted here and in `events_received` /\n`events_done`, but not in `use_cases` or `entities`.\n"
6790
6929
  },
6791
6930
  "first_ingested_at": {
6792
6931
  "type": "string",
6793
- "format": "date-time"
6932
+ "format": "date-time",
6933
+ "description": "When the first event of the correlation was accepted. Absent while none\nwas — a correlation created by a zero-event close.\n"
6794
6934
  },
6795
6935
  "last_done_at": {
6796
6936
  "type": "string",
@@ -6798,23 +6938,32 @@
6798
6938
  },
6799
6939
  "use_cases": {
6800
6940
  "type": "array",
6941
+ "description": "Per use case counts, frozen at `completed_at`.",
6801
6942
  "items": {
6802
6943
  "$ref": "#/components/schemas/CorrelationUseCaseCounts"
6803
6944
  }
6804
6945
  },
6805
6946
  "entities": {
6806
6947
  "type": "array",
6807
- "description": "Every entity the correlation's events wrote.",
6948
+ "description": "Every entity the correlation's events wrote before it completed, one entry\nper entity with its writes merged (see `operation` and `unique_ids`).\nA correlation lists at most 200 entity writes; see `entities_omitted`.\n",
6808
6949
  "items": {
6809
6950
  "$ref": "#/components/schemas/ErpUpdateProcessedEntity"
6810
6951
  }
6811
6952
  },
6953
+ "entities_omitted": {
6954
+ "type": "integer",
6955
+ "description": "Entity writes of the correlation's events that are not in `entities`,\nbecause the list was full (at most 200), or because an event's record could\nnot be written and the event ended `error` with `PROCESSING_STATE_LIMIT`.\nThe entities may well have been written; they are only missing from the\nlist, so a `wait_for_entities` requirement cannot be met by them — one that\nno listed entity satisfies is undecided instead (see\n`undecided_entity_requirements`). Present only when there are any.\n"
6956
+ },
6812
6957
  "errors": {
6813
6958
  "type": "array",
6814
- "description": "Every failure in the correlation, whatever `completion` says.",
6959
+ "description": "The correlation's first 50 failures, whatever `completion` says.\n`errors_total` counts them all.\n",
6815
6960
  "items": {
6816
6961
  "$ref": "#/components/schemas/ErpUpdateProcessingError"
6817
6962
  }
6963
+ },
6964
+ "errors_total": {
6965
+ "type": "integer",
6966
+ "description": "Every failure in the correlation, including those not listed in `errors` (the first 50 are) and those of events that arrived after completion (`after_completion`), which do not change the answer."
6818
6967
  }
6819
6968
  }
6820
6969
  },
@@ -6838,6 +6987,16 @@
6838
6987
  "error"
6839
6988
  ],
6840
6989
  "description": "Why the wait ended. See the operation description."
6990
+ },
6991
+ "undecided_entity_requirements": {
6992
+ "type": "array",
6993
+ "description": "The `wait_for_entities` requirements this record cannot decide, each\nexactly as you passed it. Present only when the correlation omitted\nentities (`entities_omitted`) and a requirement is satisfied by none of\nthe listed ones: the list holds at most 200 entity writes, so the entity\nmay well have been written, and an omitted one is never listed later.\nSuch a requirement is neither met nor missing — it never makes\n`completion` `error`, and the wait still ends `complete` once the\ncorrelation completes. Look the entity up yourself, e.g. by its unique\nid through the entity API.\n",
6994
+ "items": {
6995
+ "type": "string"
6996
+ },
6997
+ "example": [
6998
+ "contract[billing_account_number=4711]"
6999
+ ]
6841
7000
  }
6842
7001
  }
6843
7002
  }
@@ -6860,6 +7019,10 @@
6860
7019
  "type": "string",
6861
7020
  "description": "The selector key that is not a unique id of `slug`."
6862
7021
  },
7022
+ "use_case_slug": {
7023
+ "type": "string",
7024
+ "description": "The refused `wait_for_use_cases` entry, as passed: one that is not a use\ncase slug, or a slug no inbound use case of the organization (or of the\ncorrelation's integration) has.\n"
7025
+ },
6863
7026
  "allowed_keys": {
6864
7027
  "type": "array",
6865
7028
  "description": "Every unique id of `slug` across the organization's inbound use cases —\nthe keys a selector on `slug` may name. Empty when no use case writes it.\n",
@@ -6886,7 +7049,7 @@
6886
7049
  "next_cursor": {
6887
7050
  "type": "string",
6888
7051
  "nullable": true,
6889
- "description": "Cursor for the next page, or null when there are no more rows."
7052
+ "description": "Cursor for the next page; null when there are no more rows. Only null\nends the listing: a short or even empty page can still carry a cursor.\n"
6890
7053
  },
6891
7054
  "horizon_days": {
6892
7055
  "type": "integer",
@@ -12264,6 +12427,10 @@
12264
12427
  "items": {
12265
12428
  "$ref": "#/components/schemas/RelationUniqueIdField"
12266
12429
  }
12430
+ },
12431
+ "required": {
12432
+ "type": "boolean",
12433
+ "description": "The entity is incomplete without this relation. On an interactive event, a\nrelation that cannot be set — even because the payload carried no reference —\nends the event `partial` and leaves the entity out of `entities`. Without it,\na relation whose unique ids the payload left empty is intentionally unset: the\nentity is written and reported without it. A relation whose reference the\npayload sent only in part (one unique id present, another missing) is\nunresolved either way. Bulk processing is the same with or without it.\n"
12267
12434
  }
12268
12435
  }
12269
12436
  },
@@ -13440,7 +13607,7 @@
13440
13607
  },
13441
13608
  "MonitoringCode": {
13442
13609
  "type": "string",
13443
- "description": "The monitoring code taxonomy — every code the Integration Toolkit itself emits,\nwith a fixed level (see the published code reference at\nhttps://docs.epilot.io/docs/integrations/integration-toolkit/monitoring/codes).\n\nThis schema exists so consumers can import the union as a type. It is\ndeliberately NOT applied to `MonitoringEventV2.code` or to the query filter:\nalongside these, the secure proxy emits an unbounded `HTTP_{status}` family for\nan upstream refusal, so constraining those fields to this enum would reject a\nlegitimate value — `code` stays a plain string there on purpose.\n\nKept in lock-step with the `MonitoringErrorCode` taxonomy in erp-utils by\n`monitoring-code-enum.test.ts`, which fails if the two diverge.\n",
13610
+ "description": "The monitoring code taxonomy — every code the Integration Toolkit itself emits,\nwith a fixed level (see the published code reference at\nhttps://docs.epilot.io/docs/integrations/integration-toolkit/monitoring/codes).\n\nThis schema exists so consumers can import the union as a type. It never\nconstrains a field on its own: alongside these, the secure proxy emits an\nunbounded `HTTP_{status}` family for an upstream refusal, so constraining a\ncode field to this enum would reject a legitimate value. `MonitoringEventV2.code`\nreferences it only as one `anyOf` branch next to a plain string, which types the\nfield with the taxonomy without narrowing it; the query filter stays a plain\nstring.\n\nKept in lock-step with the `MonitoringErrorCode` taxonomy in erp-utils by\n`monitoring-code-enum.test.ts`, which fails if the two diverge.\n",
13444
13611
  "enum": [
13445
13612
  "ACK_CONFIRMED",
13446
13613
  "ACK_PENDING",
@@ -13509,6 +13676,7 @@
13509
13676
  "MSG_LATE_ARRIVAL",
13510
13677
  "OAUTH2_TOKEN_FAILURE",
13511
13678
  "PAYLOAD_TOO_LARGE",
13679
+ "PROCESSING_STATE_LIMIT",
13512
13680
  "PRUNE_SCOPE_COMPLETED",
13513
13681
  "PRUNE_SCOPE_PARTIAL_FAILURE",
13514
13682
  "RECURSION_DEPTH_EXCEEDED",
@@ -13602,8 +13770,15 @@
13602
13770
  "description": "Event outcome level"
13603
13771
  },
13604
13772
  "code": {
13605
- "type": "string",
13606
- "description": "Taxonomy code (e.g. OAUTH2_TOKEN_FAILURE, HTTP_502). Empty for success. One of `MonitoringCode`, or an `HTTP_{status}` family value from the secure proxy — which is why this is a string rather than that enum."
13773
+ "description": "Taxonomy code (e.g. OAUTH2_TOKEN_FAILURE, HTTP_502). Empty for success. One of `MonitoringCode`, or an `HTTP_{status}` family value from the secure proxy — which is why the second branch is a plain string: the field is typed with the taxonomy without being narrowed to it.",
13774
+ "anyOf": [
13775
+ {
13776
+ "$ref": "#/components/schemas/MonitoringCode"
13777
+ },
13778
+ {
13779
+ "type": "string"
13780
+ }
13781
+ ]
13607
13782
  },
13608
13783
  "message": {
13609
13784
  "type": "string",
@@ -13756,7 +13931,7 @@
13756
13931
  },
13757
13932
  "in_flight": {
13758
13933
  "type": "integer",
13759
- "description": "Events that have reported something but no final outcome yet: only mapping or relation caveats so far (the success row comes last, from the final stage), or a failed attempt the queue will retry. An event stuck here with an old `last_activity_at` was most likely lost on its final attempt (processor crash) — a stall, not a slow run."
13934
+ "description": "Events that have reported something but no final outcome yet: only mapping or relation caveats so far (the success row comes last, from the final stage), or a failed attempt the queue will retry. An event stuck here with an old `last_activity_at` was most likely lost on its final attempt (processor crash) — a stall, not a slow run. Includes split events still waiting on some of their writes."
13760
13935
  },
13761
13936
  "retrying": {
13762
13937
  "type": "integer",
@@ -13791,29 +13966,36 @@
13791
13966
  },
13792
13967
  "MonitoringRunEntriesV2": {
13793
13968
  "type": "object",
13794
- "description": "The run's monitoring rows counted by level, retry markers included — the same rows its trace lists. Unlike the event counts above, one event can contribute many rows (one per entity written, plus every caveat and retry).",
13969
+ "description": "The run's monitoring rows counted by level — the rows its trace lists, correlation-level rows (wait outcomes, closes) included. Unlike the event counts above, one event can contribute many rows (one per entity written, plus every caveat and retry). Retry markers count under `retry` only, so a failed attempt that later succeeds is not an error; `total` is the sum of the others.",
13795
13970
  "required": [
13796
13971
  "total",
13797
13972
  "error",
13798
13973
  "warning",
13799
13974
  "success",
13800
- "info"
13975
+ "info",
13976
+ "retry"
13801
13977
  ],
13802
13978
  "properties": {
13803
13979
  "total": {
13804
13980
  "type": "integer"
13805
13981
  },
13806
13982
  "error": {
13807
- "type": "integer"
13983
+ "type": "integer",
13984
+ "description": "Error rows that will not be retried."
13808
13985
  },
13809
13986
  "warning": {
13810
- "type": "integer"
13987
+ "type": "integer",
13988
+ "description": "Warning rows that are caveats, not retry markers."
13811
13989
  },
13812
13990
  "success": {
13813
13991
  "type": "integer"
13814
13992
  },
13815
13993
  "info": {
13816
13994
  "type": "integer"
13995
+ },
13996
+ "retry": {
13997
+ "type": "integer",
13998
+ "description": "Retry markers (`will_retry` in `detail`) — attempts the queue delivers again."
13817
13999
  }
13818
14000
  }
13819
14001
  },
@@ -14142,7 +14324,7 @@
14142
14324
  },
14143
14325
  "responses": {
14144
14326
  "BadRequest": {
14145
- "description": "Bad request",
14327
+ "description": "Bad request. `message` says what is wrong. A request refused by the request\nvalidation, before any handler ran, also carries `status: 400` and the raw\nvalidation errors under `error`.\n",
14146
14328
  "content": {
14147
14329
  "application/json": {
14148
14330
  "schema": {
@@ -14162,7 +14344,7 @@
14162
14344
  }
14163
14345
  },
14164
14346
  "Unauthorized": {
14165
- "description": "Unauthorized request",
14347
+ "description": "Unauthorized: the request carries no bearer token. The API gateway answers\n`{\"message\": \"Unauthorized\"}`. A token that is present but invalid or expired\nis refused with 403, not 401.\n",
14166
14348
  "content": {
14167
14349
  "application/json": {
14168
14350
  "schema": {
@@ -14172,7 +14354,7 @@
14172
14354
  }
14173
14355
  },
14174
14356
  "Forbidden": {
14175
- "description": "Forbidden - insufficient permissions",
14357
+ "description": "Forbidden: the token lacks a permission the operation requires, or it is\ninvalid or expired — the API gateway refuses those with\n`{\"message\": \"Forbidden\"}` before the request reaches the API.\n",
14176
14358
  "content": {
14177
14359
  "application/json": {
14178
14360
  "schema": {
@@ -14461,7 +14643,10 @@
14461
14643
  "properties": {
14462
14644
  "correlation_id": {
14463
14645
  "type": "string",
14464
- "description": "The request's correlation id, echoed, or the one generated for it when the caller omitted it. This is the id to pass to `GET /v3/erp/updates/events?correlation_id=...` to wait for these events. Omitted when the request carried no correlation id and none was generated (v1 and v2).\n"
14646
+ "description": "The request's correlation id, echoed, or the one generated for it when the caller omitted it. This is the id to pass to `GET /v3/erp/updates/events?correlation_id=...` to wait for these events. v3 and v2 generate one when the request has none, so they always return it, except that v2 omits it for an empty `correlation_id`. v1 never generates one: it echoes a non-empty `meta.correlation_id` and otherwise omits the field.\n"
14647
+ },
14648
+ "correlation_close": {
14649
+ "$ref": "#/components/schemas/CorrelationCloseResult"
14465
14650
  },
14466
14651
  "results": {
14467
14652
  "type": "array",
@@ -14488,7 +14673,7 @@
14488
14673
  },
14489
14674
  "interactive": {
14490
14675
  "type": "boolean",
14491
- "description": "Whether the event was PROCESSED as interactive - its processing record exists and the correlation wait works on it. Present only when interactive processing was requested or the use case is configured `always`. `true` with a `lane` other than `interactive` means the organization was over its rate and the event was carried on its normal lane; the wait still works, it is just slower. `false` means the use case does not allow interactive processing, and `message` says so.\n"
14676
+ "description": "Whether the event was PROCESSED as interactive - its processing record exists and the correlation wait works on it. Present only when interactive processing was requested or the use case is configured `always`. `true` with a `lane` other than `interactive` means the organization was over its rate and the event was carried on its normal lane; the wait still works, it is just slower. `false` means the event was processed on its normal lane and cannot be waited on, and `message` says why: the use case does not allow interactive processing; the event's correlation id is not 1-255 letters, digits, hyphens and underscores (only on an `always` use case - a request that asks for interactive with such an id is a 400); the correlation is at its event limit or belongs to another integration; or interactive processing is unavailable.\n"
14492
14677
  },
14493
14678
  "lane": {
14494
14679
  "type": "string",
@@ -14528,7 +14713,10 @@
14528
14713
  "properties": {
14529
14714
  "correlation_id": {
14530
14715
  "type": "string",
14531
- "description": "The request's correlation id, echoed, or the one generated for it when the caller omitted it. This is the id to pass to `GET /v3/erp/updates/events?correlation_id=...` to wait for these events. Omitted when the request carried no correlation id and none was generated (v1 and v2).\n"
14716
+ "description": "The request's correlation id, echoed, or the one generated for it when the caller omitted it. This is the id to pass to `GET /v3/erp/updates/events?correlation_id=...` to wait for these events. v3 and v2 generate one when the request has none, so they always return it, except that v2 omits it for an empty `correlation_id`. v1 never generates one: it echoes a non-empty `meta.correlation_id` and otherwise omits the field.\n"
14717
+ },
14718
+ "correlation_close": {
14719
+ "$ref": "#/components/schemas/CorrelationCloseResult"
14532
14720
  },
14533
14721
  "results": {
14534
14722
  "type": "array",
@@ -14555,7 +14743,7 @@
14555
14743
  },
14556
14744
  "interactive": {
14557
14745
  "type": "boolean",
14558
- "description": "Whether the event was PROCESSED as interactive - its processing record exists and the correlation wait works on it. Present only when interactive processing was requested or the use case is configured `always`. `true` with a `lane` other than `interactive` means the organization was over its rate and the event was carried on its normal lane; the wait still works, it is just slower. `false` means the use case does not allow interactive processing, and `message` says so.\n"
14746
+ "description": "Whether the event was PROCESSED as interactive - its processing record exists and the correlation wait works on it. Present only when interactive processing was requested or the use case is configured `always`. `true` with a `lane` other than `interactive` means the organization was over its rate and the event was carried on its normal lane; the wait still works, it is just slower. `false` means the event was processed on its normal lane and cannot be waited on, and `message` says why: the use case does not allow interactive processing; the event's correlation id is not 1-255 letters, digits, hyphens and underscores (only on an `always` use case - a request that asks for interactive with such an id is a 400); the correlation is at its event limit or belongs to another integration; or interactive processing is unavailable.\n"
14559
14747
  },
14560
14748
  "lane": {
14561
14749
  "type": "string",