Skip to Content
IntegrationsRecord outcomes API

Record outcomes API

A run finishing is not the same as the work holding up. The ticket gets reopened, the order gets refunded, the pull request gets reverted. This API takes that later fact, keyed by the record rather than by the run, and labels every run that acted on the record.

For it to find the runs, they must carry a record key such as ("zendesk_ticket", "4812"). You send the event; Morse does the matching.

How it works

Auth

Send an API key with write access as a bearer token:

Authorization: Bearer mhq_…

Environment keys and full-access personal keys work. A read-only key is refused with 403. If the key is scoped to one environment, the event is stored with that environment and only labels runs from it: a staging key never labels production runs.

Send an event

POST https://api.morsehq.dev/api/v1/outcomes/by-key

Send one event as the body, or up to 100 in a list under events:

{ "key_type": "zendesk_ticket", "key_value": "4812", "success": false, "outcome": "reopened", "occurred_at": "2026-10-12T10:15:00Z", "event_id": "ticket-4812-reopened-2026-10-12T10:15:00Z" }
{ "events": [ { "key_type": "github_pr", "key_value": 1123, "success": true, "outcome": "merged" }, { "key_type": "github_pr", "key_value": 1124, "success": false, "outcome": "closed_unmerged" } ], "dry_run": false }
FieldTypeNotes
key_typestringRequired. Lower-cased. Must match ^[a-z0-9][a-z0-9_.-]{0,63}$.
key_valuestringRequired. 1–256 characters after trimming. An unquoted integer is accepted.
successbooleanRequired. Did the record end up in a good state?
outcomestringOptional label, up to 255 characters, such as reopened, refunded, merged.
occurred_atdatetimeOptional. When the event happened (ISO 8601). Defaults to when Morse receives it.
event_idstringOptional, up to 128 characters. Makes the call safe to retry; see Retries.
stateobjectOptional. A typed record state (resolved or reopened) that outcome rules read.
dry_runbooleanOptional, top level. Computes the result without labeling any run or storing the event.

The state field is optional and additive; an event without it behaves as described on this page. Its fields are in Outcome rules.

For pull requests connected through the GitHub connector, the key value is owner/repo#number, not the bare number used in the examples here.

The response

A single event returns one result. A list returns {"results": [...]}, one per event and in the same order.

{ "event_id": "ticket-4812-reopened-2026-10-12T10:15:00Z", "matched": 3, "applied": 2, "kept_higher_confidence": 1, "kept_newer": 0, "pending": false, "truncated": false, "key_type_seen": true, "trace_ids": ["…", "…"] }
FieldMeaning
matchedRuns that carry the key, started in time, in your environment scope.
appliedRuns whose label was written or changed by this event.
kept_higher_confidenceRuns left alone because they already hold a label you set yourself or confirmed.
kept_newerRuns left alone because they already hold a record label from a later event.
pendingtrue when nothing matched yet. The event is kept and applied if a matching run arrives; see below.
truncatedtrue when more than 1,000 runs matched. The 1,000 most recent were labeled.
key_type_seenfalse when your organization has never sent a run with this key_type.
trace_idsUp to 100 of the labeled runs.

pending: true with key_type_seen: false is almost always a typo, such as zendesk_tickets for zendesk_ticket. You see it on the first call instead of after a week of silence.

Rules

Provenance. A label written by this API has provenance record: the business outcome of the record the run acted on. It ranks between a per-run webhook outcome and an inferred one. It never overwrites a label set in code with set_outcome(), one a person set in the dashboard, a classifier-confirmed one, or a per-run webhook one. Those runs are counted in kept_higher_confidence.

Time bound. An event only labels runs that started at or before its occurred_at, plus 5 minutes of clock-skew allowance. A “reopened” event does not label the runs that handled the reopen.

Ordering. A record label replaces an earlier record label only if the new event’s occurred_at is not older. If “reopened” (10:15) is applied and a “solved” event for 09:00 arrives late, the run keeps “reopened” and the late event is counted in kept_newer.

Runs that arrive after the event. Every event from the last 7 days is applied to runs that gain the key afterwards, under the same rules. That covers runs still open when the event arrived. After 7 days an event that matched nothing is expired.

Environment scope. An environment-scoped key only matches runs from that environment.

Fan-out. At most 1,000 runs per event, most recent first. Beyond that the response says truncated: true.

Retries

event_id is the idempotency key. If you send the same event_id again, Morse returns the first result and writes nothing, so a webhook that retries does not double-apply. Build it from facts that do not change between retries: the record, the transition and its time.

Without an event_id, every call is a new event.

Errors

Validation failures return 422 with a field-level message. 401 means no valid key, 403 a key without write access. The endpoint shares your key’s write rate limit; over it you get 429 with Retry-After. Another organization’s keys are never matched.

Check what happened: GET /api/v1/outcomes/events

Lists your recent record-outcome events, newest first, with their status and counts.

curl -H "Authorization: Bearer $MORSE_API_KEY" \ "https://api.morsehq.dev/api/v1/outcomes/events?status=pending&limit=20"
ParameterNotes
key_typeOnly events for this key type.
statusapplied, pending, expired or no_effect.
limit1–200 (default 50).
beforeA datetime; return events received before it. Use next_before to page.

Each event has id, event_id, key_type, key_value, success, outcome, occurred_at, received_at, environment_id, status, applied and matched. The response also carries next_before, which is null on the last page.

StatusMeaning
appliedAt least one run was labeled.
pendingNothing carries the key yet. Still inside the 7-day window.
expiredNothing carried the key within 7 days.
no_effectRuns matched, but none changed: all held a higher-confidence or newer label.

Counts are derived from the label history, not from a counter that can drift. The same numbers appear per agent under Capture health on the agent page.

How to use it

⚠️

Requires SDK 0.7.0 (not yet released; 0.6.0 does not have this). link_record() in the examples below is a 0.7.0 SDK call. On 0.6.0, attach the record key over REST or OTLP (see Record keys and versions).

Morse does not poll your tools. You send your events: from a CI step, a webhook, a script. Two common ones follow. Use the same pattern for refunds, chargebacks, reverts or any other record transition you can observe.

Pull requests: a GitHub Actions step

Your agent opens or touches a pull request and calls link_record("github_pr", pr.number). When the pull request closes, send whether it merged:

name: Send PR outcome to Morse on: pull_request: types: [closed] jobs: send-outcome: runs-on: ubuntu-latest steps: - name: Send the outcome env: MORSE_API_KEY: ${{ secrets.MORSE_API_KEY }} PR_NUMBER: ${{ github.event.pull_request.number }} MERGED: ${{ github.event.pull_request.merged }} CLOSED_AT: ${{ github.event.pull_request.closed_at }} run: | jq -n \ --arg value "$PR_NUMBER" \ --argjson merged "$MERGED" \ --arg at "$CLOSED_AT" \ '{ key_type: "github_pr", key_value: $value, success: $merged, outcome: (if $merged then "merged" else "closed_unmerged" end), occurred_at: $at, event_id: ("github-pr-" + $value + "-" + $at) }' | curl --fail-with-body -sS -X POST https://api.morsehq.dev/api/v1/outcomes/by-key \ -H "Authorization: Bearer $MORSE_API_KEY" \ -H "Content-Type: application/json" \ -d @-

merged is true only for a merged pull request, so a closed-without-merging pull request is sent as success: false. A pull request that is reopened and closed again has a new closed_at, so it gets a new event_id. If the pull request is later reverted, send a second event from whatever observes the revert.

Support tickets: a helpdesk trigger

Most helpdesks can call a webhook when a ticket changes status. Configure a trigger on the transitions you care about (reopened, solved, escalated) and have it send a body like this, with your tool’s template variables in place of the placeholders:

{ "key_type": "helpdesk_ticket", "key_value": "{{ticket.id}}", "success": false, "outcome": "reopened", "occurred_at": "{{ticket.updated_at}}", "event_id": "ticket-{{ticket.id}}-reopened-{{ticket.updated_at}}" }

The key type is yours to choose; use the same string in link_record("helpdesk_ticket", ticket.id). If your tool’s webhook can set an Authorization header, point it straight at the endpoint. Otherwise send it through a small relay (a serverless function or an automation step) that adds the header. Template variables that render as bare numbers are fine, since integers are accepted for key_value.

Send one trigger per transition. A “solved” trigger with success: true and a “reopened” trigger with success: false are two events about the same ticket; the later occurred_at wins.

Try it without writing anything

curl -X POST https://api.morsehq.dev/api/v1/outcomes/by-key \ -H "Authorization: Bearer $MORSE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"key_type":"zendesk_ticket","key_value":"4812","success":false,"outcome":"reopened","dry_run":true}'

dry_run returns the same counts, so you can check matched, key_type_seen and kept_higher_confidence against real data before wiring up the sender.

Last updated on