Events, API and typed errors
UMP adopts existing standards for its wire formats instead of inventing them: CloudEvents for event envelopes, Standard Webhooks for signing, OpenAPI for the service and RFC 9457 for errors.¶
Event envelope. CloudEvents 1.0.2 with the required id, source, specversion and type, plus UMP extension attributes artifactid, artifactversion, artifactseq and scope. Events carry IDs and states only, never data values (the claim-check pattern from modeling-api’s webhooks). Webhook deliveries use the CloudEvents HTTP binding in structured mode (application/cloudevents+json) and are signed per Standard Webhooks. webhook-id MUST equal the CloudEvents id. The signature covers webhook-id.webhook-timestamp.body, where body is the exact bytes sent. Signatures are v1 (HMAC-SHA256 with a whsec_ secret) or v1a (Ed25519). During key rotation, the header MAY carry several space-delimited signatures. Internal consumers on Pub/Sub use the CloudEvents Pub/Sub binding, are authenticated by the platform rather than by signatures, and deduplicate on the ce-id attribute.¶
Delivery rules¶
- At-least-once delivery, so consumers MUST be idempotent.¶
- Identity: consumers MUST deduplicate on (
source,id). On webhook delivery,webhook-idMUST equal the CloudEventsid.¶ - Replay protection: deliveries with a timestamp more than five minutes from the receiver’s clock are rejected (proposed tolerance).¶
- Producers write events through a transactional outbox so a state change and its event cannot diverge.¶
- Ordering: every event carries the UMP extension attribute
artifactseq, an integer that increases strictly for each event emitted about one artifact, whatever its type.artifactversionis the content revision the event refers to (section 7) and MAY repeat across events. A consumer MAY discard an event whoseartifactseqis at or below the last one it applied for thatartifactid. It MUST NOT discard an event only because itsartifactversionwas already seen.¶ - State: flags such as stale, withdrawn and certified are state, not deltas. A consumer that receives such an event, or detects a gap in
artifactseq, SHOULD read the current state from the API (claim check) before acting. A stale flag clears only when the artifact is rescored against a newer revision, never because of an event.¶ - Events with no
artifactid(for exampleintegrity.broken) are not ordered and are processed on arrival.¶ - Undeliverable events go to a dead-letter queue; a replay tool re-sends by lineage query (“all events for scenarios downstream of model X since date Y”). A replay keeps the original CloudEvents
idandwebhook-id, so receivers deduplicate it, and is re-signed with a freshwebhook-timestampso it passes replay protection.¶
Core events. dataset.registered, model.version.ready, endpoint.promoted, test.plan.locked, test.result.published, calibration.flagged, evidence.aging, scenario.stale, capability.certified, capability.deprecated, capability.withdrawn, decision.review_due, integrity.broken, agent.release.held, job.completed, job.failed.¶
On the wire, each type is prefixed with the reverse-DNS namespace registered at publication (for example org.ump.model.version.ready). This document uses the short names.¶
API. One OpenAPI service. The OpenAPI 3.1 document published with this draft is normative for paths, request bodies, responses and problem types. The block below is an informative summary. The document’s schemas reference the artifact JSON Schemas by $id. Requests are validated from metadata before compute; slow jobs return an ID.¶
GET /v1/models/{id}/metadata # capability handshakeGET /v1/artifacts?kind=model&state=validatedGET /v1/artifacts/{id}/evidence-cardGET /v1/lineage/{id}?direction=downstream&depth=nGET /v1/log/checkpoint # latest checkpoint (C2SP signed note) and its RFC 3161 tokenGET /v1/log/checkpoints?since= # anchored checkpoints, cursor pagedGET /v1/log/proof/{record_id}?tree_size= # leaf index, tree size, audit path, checkpointGET /v1/log/consistency?first=&second= # RFC 9162 consistency proof between tree sizesGET /v1/jobs/{id}POST /v1/jobs/{id}:cancelGET /v1/metaPOST /v1/models/{id}:validate # metadata-only checkPOST /v1/models/{id}:{request} # any standard (section 8) or family-profile (section 16) request the manifest declaresPOST /v1/scenarios | /v1/test-plans | /v1/decisions # always draftsPOST /v1/artifacts/{id}:transitionProof responses MUST name the checkpoint they verify against.¶
Idempotency. Every POST that creates a record or starts compute MUST carry an Idempotency-Key header (draft-ietf-httpapi-idempotency-key-header). A missing key returns IDEMPOTENCY_KEY_MISSING (400). The server keeps keys for at least 24 hours (proposed) and publishes its retention. A retry with the same key and body returns the original response. The same key with a different body returns IDEMPOTENCY_CONFLICT (422). A retry while the first request is still running returns IDEMPOTENCY_KEY_IN_FLIGHT (409). A draft decision, test plan or scenario created twice under one key is one record.¶
Jobs. A call that can take more than 30 seconds (proposed) returns 202 Accepted with Location: /v1/jobs/{id}. A job has state (queued, running, succeeded, failed, cancelled), created_at, updated_at and steps. Once terminal, it has either result (a link to the artifact or response) or problem (an RFC 9457 object). POST /v1/jobs/{id}:cancel requests cancellation. Terminal states never change. Results are retained for at least the idempotency window. Completion is also announced by job.completed or job.failed, carrying the job ID.¶
Listing. List endpoints use cursor paging: page_size (maximum 500, proposed) and page_token in; items and next_page_token out. Order is stable by (created_at, id), and an empty next_page_token means the list is complete. Lineage queries take depth up to a server maximum and return truncated: true with a next_page_token when the graph exceeds it. Impact analysis MUST NOT treat a truncated result as complete.¶
Access. Tools call the API with OAuth 2.0 client credentials, or platform workload identity, bound to an audience (RFC 8707) and carrying the OAuth scopes of section 17 and the data scopes they may read. A data scope is written <kind>:<id> (for example client:northwind). Approval transitions require an interactive user credential plus the approver’s signature (section 18). A client-credentials token MUST receive APPROVER_INVALID on any :transition into validated or rejected.¶
Gate mode. A request MAY carry on_gate_fail (shape in section 15) as a top-level JSON body member, or the string form in the UMP-On-Gate-Fail request header. If both are present and differ, the server MUST reject the request with GATE_MODE_CONFLICT (422). Servers MUST echo the effective mode in the UMP-On-Gate-Fail response header.¶
Version and metadata. Servers MUST send UMP-Spec-Version with the contract SemVer on every response. They MUST serve GET /v1/meta, returning that version, supported_spec_versions, the supported conformance classes and the gate catalogue version. Version negotiation is defined in section 21. A health endpoint is a deployment concern and out of scope.¶
Fallback notes and typed errors. Under the default on_gate_fail: "fallback", a gate that falls back still returns 200 with the result and a fallbacks array; each entry carries the code below, what was assumed, and how the interval changed. The same codes come back as RFC 9457 problem details (RFC 7807 is obsolete) only for hard stops, for operations with no defensible basis, or when the caller or account policy asks for "strict". A problem details object carries the RFC 9457 members type, title, status, detail and instance, plus the UMP extension members code (stable, UPPER_SNAKE), gate_id, pointer (a JSON Pointer into the request), suggested_action, errors (below) and untrusted_text, which keeps text from outside the platform apart from platform text. The extension set acknowledges modeling-api’s problem shape.¶
"fallbacks": [ {"code": "ESTIMAND_UNDECLARED", "gate": "G-EST-01", "field": "baseline", "assumed": "zero_spend", "effect": "interval widened to cover current_plan baseline"}, {"code": "DEPENDENCE_ASSUMED", "gate": "G-DEP-01", "envelope_kind": "bound", "assumed": "independence", "effect": "interval is envelope of independent and comonotone"}]Code registry. The table below is normative. Its published form is the machine-readable registry file (section 21, Registries). That file MAY also carry codes registered by an implementation for its own operational notes; those are not part of this standard. Kind is Fallback note, Hard stop, Error or Note. For a fallback note, Status is the status returned in strict mode; under the default mode the code rides in fallbacks with 200. A Note is informational and never becomes a problem. The type column gives the path after the problem base.¶
| Code | Kind | type | Status | Raised by (gate) | Retryable | Raised when |
|---|---|---|---|---|---|---|
ESTIMAND_UNDECLARED¶ | Fallback note | estimand-undeclared | 422 | G-EST-01, G-BM-01 | No | A required estimand field is missing or undeclared; the registered default was used |
ESTIMAND_ALIGNED¶ | Fallback note | estimand-aligned | 409 | G-EST-02 | No | Quantities with different estimands were aligned by re-asking the model or by an assumption |
DEPENDENCE_ASSUMED¶ | Fallback note | dependence-assumed | 422 | G-DEP-01 | No | A portfolio operation ran on a model without joint dependence. Member envelope_kind is bound or range; an EVOI without joint dependence is a range, and G-MAT-01 uses its upper end |
UNCERTAINTY_IMPUTED¶ | Fallback note | uncertainty-imputed | 422 | G-UNC-01, G-BM-07 | No | A result had no uncertainty and a library-based band was used |
PSIS_UNRELIABLE¶ | Fallback note | psis-unreliable | 409 | G-CAL-05 | No | Reweighting failed its diagnostics; an approximate summary update was used and a refit queued |
EVIDENCE_EXCLUDED¶ | Fallback note | evidence-excluded | 200 (stays a note in strict mode) | G-EXP-05, G-BM-02 | No | A class D result, a class E result offered for a non-brand family, a contaminated test or a failed capability was left out |
OUTSIDE_GRID¶ | Fallback note | outside-grid | 422 | G-SCN-03 | No | A Tier C score went beyond the declared grid and was extrapolated |
VENDOR_UNVALIDATED¶ | Fallback note | vendor-unvalidated | 409 | G-VEN-01 | No | An unvalidated table fed a decision at the Full materiality tier |
NOT_VALIDATED¶ | Fallback note | not-validated | 409 | G-SCN-01 | No | The newest endpoint revision is unvalidated; the last validated revision was used |
DATA_UNREGISTERED¶ | Fallback note | data-unregistered | 409 | G-DATA-01 | No | A raw delivery was used before it was registered with fingerprint, source and range |
TOTALS_GAP¶ | Fallback note | totals-gap | 409 | G-DATA-02 | No | Extracted totals do not tie to printed totals |
UNCHECKED_EXTRACTION¶ | Fallback note | unchecked-extraction | 409 | G-DATA-03 | No | Values above materiality were used without double keying |
LEAKAGE_RISK¶ | Fallback note | leakage-risk | 409 | G-MOD-03 | No | The model design risks leaking the outcome into its inputs |
UNVALIDATED_METHOD¶ | Fallback note | unvalidated-method | 409 | G-ACC-01 | No | The method has not been tested against the acceptance bar |
ACCEPTANCE_UNSUBSTANTIATED¶ | Fallback note | acceptance-unsubstantiated | 409 | G-ACC-01 | No | Acceptance was claimed without a linked acceptance report |
NOT_PREREGISTERED¶ | Fallback note | not-preregistered | 409 | G-EXP-01 | No | A test result was used without a pre-registered design |
CARRYOVER_ADJUSTED¶ | Fallback note | carryover-adjusted | 409 | G-EXP-04 | No | Test duration was below the carryover floor and the result was adjusted |
EVIDENCE_AGED¶ | Fallback note | evidence-aged | 409 | none (section 9, Ageing) | No | Evidence past its use-by date was used |
DEFAULT_EXCISION¶ | Fallback note | default-excision | 409 | G-CAL-02 | No | The default excision window was applied |
DEFAULT_SPLIT¶ | Fallback note | default-split | 409 | G-CAL-03 | No | The default split was applied |
ATTACH_ONLY¶ | Fallback note | attach-only | 409 | G-CAL-04 | No | Evidence was attached to the model without updating it |
INDEPENDENT_DRAWS¶ | Fallback note | independent-draws | 409 | G-SCN-04 | No | A delta was computed from independent rather than paired draws |
UNTRACED¶ | Fallback note | untraced | 409 | G-REP-01 | No | A number was not traced to an artifact |
UNCITED_BEST_GUESS¶ | Fallback note | uncited-best-guess | 409 | G-NEW-01 | No | An agent gave a best guess without citing an artifact for it |
AGENT_VERSION_HELD¶ | Fallback note | agent-version-held | 409 | G-NEW-03 | No | An agent version had not passed the harness, so the previous version was kept |
FIXTURES_NOT_RERUN¶ | Fallback note | fixtures-not-rerun | 409 | G-CAP-03 | No | A capability changed without its fixtures being rerun |
AGENT_RUN_NOT_ALLOWED¶ | Fallback note | agent-run-not-allowed | 409 | G-CAP-05 | No | An agent asked to run a capability its status does not allow for agents |
UNCALIBRATED_WIDENED¶ | Fallback note | uncalibrated-widened | 409 | G-BM-03 | No | An uncalibrated model entered a benchmark and its interval was widened |
BEST_GUESS_VERTICAL¶ | Fallback note | best-guess-vertical | 409 | G-BM-05 | No | A vertical benchmark was given as a labelled best guess |
DATA_RESTATED¶ | Note | data-restated | 200 | G-DATA-04 | n/a | History in a delivery was restated |
PLAN_NOT_REVIEWED¶ | Note | plan-not-reviewed | 200 | G-MOD-01 | n/a | The model plan was not reviewed before fitting |
POST_HOC_PROVISIONAL¶ | Note | post-hoc-provisional | 200 | G-MOD-02 | n/a | A post-hoc change is provisional until reviewed |
LOW_ASSURANCE¶ | Note | low-assurance | 200 | G-EXP-02 | n/a | Test assurance is below the floor |
SPILLOVER_UNRECORDED¶ | Note | spillover-unrecorded | 200 | G-EXP-03 | n/a | The test design did not record spillover handling |
CALIBRATION_CONFLICT¶ | Note | calibration-conflict | 200 | G-CAL-01 | n/a | Conflicting calibration evidence was routed for review |
MATERIALITY_COMPUTED¶ | Note | materiality-computed | 200 | G-MAT-01 | n/a | The materiality tier was computed |
AUDIT_LEVEL_COMPUTED¶ | Note | audit-level-computed | 200 | G-AUD-01 | n/a | The audit level was computed |
LADDER_SUMMARY_OR_BENCHMARK¶ | Note | ladder-summary-or-benchmark | 200 | G-SCN-02 | n/a | A result rests on summary numbers or benchmarks |
EXPERIMENTAL_LABEL¶ | Note | experimental-label | 200 | G-CAP-01 | n/a | Output came from a sandbox capability |
PILOT_LABEL¶ | Note | pilot-label | 200 | G-CAP-02 | n/a | Output came from a pilot capability |
FORK_NEEDS_REASON¶ | Note | fork-needs-reason | 200 | G-CAP-04 | n/a | A capability fork has no recorded reason |
CAPABILITY_WITHDRAWN¶ | Note | capability-withdrawn | 200 | none (section 16) | n/a | The capability that produced the output has been withdrawn |
VERTICAL_SUPPRESSED¶ | Note | vertical-suppressed | 200 | G-BM-04 | n/a | A benchmark cell had too few contributors and was suppressed |
PARENT_CELL¶ | Note | parent-cell | 200 | G-BM-04 | n/a | The parent cell was shown in place of a suppressed cell |
SUPERSEDED_REFIT¶ | Note | superseded-refit | 200 | none (section 25.3) | n/a | Only the latest refit counts; older refits and tests filed at or before it were set aside |
HELD_FROM_POOL¶ | Note | held-from-pool | 200 | G-BM-07 | n/a | A row was held back from the benchmark pool |
RELEASE_BUDGET_SPENT¶ | Note | release-budget-spent | 200 | G-BM-08 | n/a | The release budget for the scope is spent |
LICENSE_CLIENT_ONLY¶ | Note | license-client-only | 200 | G-BM-09 | n/a | The data licence permits client-only use |
ROUTED_TO_DECISION_SCORE¶ | Note | routed-to-decision-score | 200 | G-BM-06 | n/a | A forecast was routed to the decision score |
PROPOSED_THRESHOLD¶ | Note | proposed-threshold | 200 | any gate with a proposed threshold | n/a | The result depends on a threshold that is still proposed |
MULTIMODAL_HDI¶ | Note | multimodal-hdi | 200 | none (section 5) | n/a | An hdi region is not one interval and was sent as ordered segments |
NO_GUARANTEE¶ | Note | no-guarantee | 200 | none (section 11) | n/a | A test portfolio was chosen where the greedy construction’s conditions do not hold |
NONPOSITIVE_ESTIMATE¶ | Note | nonpositive-estimate | 200 | none (section 25) | n/a | A benchmark row has a non-positive estimate or a lower bound at or below 0 and was kept |
PRIVACY_FLOOR¶ | Hard stop | privacy-floor | 403 | G-PRIV-01..03 | No | A release would fall below the privacy floor; there is no fallback |
INTEGRITY_FAILURE¶ | Hard stop | integrity-failure | 409 | G-INT-01, G-INT-03, G-INT-04 | No | A record failed integrity checks against the log; there is no fallback |
APPROVER_INVALID¶ | Hard stop | approver-invalid | 403 | G-INT-02 | No | The approver’s identity or credential is not valid for the transition |
SELF_APPROVAL_FORBIDDEN¶ | Hard stop | self-approval-forbidden | 403 | G-INT-02 | No | The approver is the submitter, owner or author |
TOOL_SEPARATION¶ | Hard stop | tool-separation | 403 | G-NEW-02 | No | A session that read untrusted text asked for a write or run tool |
HARD_STOP_NOT_OVERRIDABLE¶ | Error | hard-stop-not-overridable | 409 | none | No | An override named a hard-stop gate |
REVIEW_REQUIREMENTS_OPEN¶ | Error | review-requirements-open | 409 | none (section 15) | No | An approval was signed with review requirements open and no override reason |
GATE_STOP¶ | Error | gate-stop | 409 | any gate in strict mode without its own strict status | No | A gate stopped the step |
SCHEMA_INVALID¶ | Error | schema-invalid | 422 | none (section 21) | No | The body does not validate against its schema |
ESTIMAND_UNKNOWN_FIELD¶ | Error | estimand-unknown-field | 422 | none (section 5) | No | An estimand or other value object carries a member the reader does not recognise |
UNKNOWN_KIND¶ | Error | unknown-kind | 422 | none | No | The artifact kind is not registered |
INVALID_TRANSITION¶ | Error | invalid-transition | 409 | none (section 15) | No | The requested state transition is not allowed from the current state |
REJECTION_NEEDS_COMMENT¶ | Error | rejection-needs-comment | 422 | none | No | A rejection was sent without a comment |
GATE_MODE_CONFLICT¶ | Error | gate-mode-conflict | 422 | none | No | The body on_gate_fail and the UMP-On-Gate-Fail header differ |
IDEMPOTENCY_KEY_MISSING¶ | Error | idempotency-key-missing | 400 | none | No | A POST that creates a record or starts compute has no Idempotency-Key |
IDEMPOTENCY_CONFLICT¶ | Error | idempotency-conflict | 422 | none | No | An Idempotency-Key was reused with a different body |
IDEMPOTENCY_KEY_IN_FLIGHT¶ | Error | idempotency-key-in-flight | 409 | none | Yes | A retry arrived while the first request under its key was still running |
SPEC_VERSION_UNSUPPORTED¶ | Error | spec-version-unsupported | 415 | none (section 21) | No | Received content has an unsupported MAJOR version |
SPEC_VERSION_NOT_ACCEPTABLE¶ | Error | spec-version-not-acceptable | 406 | none (section 21) | No | No supported version range overlaps the Accept profile |
UNSUPPORTED_MEDIA_TYPE¶ | Error | unsupported-media-type | 415 | none | No | The Content-Type is not supported |
OPERATION_UNAVAILABLE¶ | Error | operation-unavailable | 501 | none (section 8) | No | The manifest marks the request unavailable or computed_by_ump for this engine or tier |
NOT_FOUND¶ | Error | not-found | 404 | none | No | The resource does not exist or is outside the caller’s scopes |
CONFLICT¶ | Error | conflict | 409 | none | No | Stored state conflicts with the request |
UNAUTHORIZED¶ | Error | unauthorized | 401 | none (access control) | No | The request carries no valid credential |
FORBIDDEN¶ | Error | forbidden | 403 | none (access control) | No | The credential lacks the scope for the request |
UPSTREAM_UNAVAILABLE¶ | Error | upstream-unavailable | 502 | none | Yes | An engine or upstream service did not answer |
SERVICE_UNAVAILABLE¶ | Error | service-unavailable | 503, with Retry-After | none | Yes | The service is temporarily unable to handle the request |
INTERNAL¶ | Error | internal | 500 | none | Yes, with the same Idempotency-Key | An unexpected server error |
Each code has exactly one type URI and one status. type MUST be the registered URI (base https://ump.dev/problems/, fixed at publication, section 24), formed as the base followed by the code in lower case with underscores replaced by hyphens, and code repeats it in UPPER_SNAKE. Problem details carry gate_id when a gate raised the problem. Status rule: 422 when the input lacks a declaration the gate needs; 409 when stored state conflicts with the request; 403 when a privacy, approver or tool-separation hard stop applies; 409 when a record fails integrity checks against the log (INTEGRITY_FAILURE). When several problems are returned at once, the top-level object describes the most urgent one, and the extension member errors lists every problem as {code, gate_id, pointer, detail}. Hard stops come first, then strict-mode failures in gate catalogue order. Gate stops are never retryable. Clients MAY retry only problems marked retryable. Hard stops MUST NOT be lifted by strict-mode settings, policy floors or reviewer action.¶