docs

Deployments API

Declare deployments over HTTP with a deploy key, including request fields, limits, status updates, merging and response codes.

Send one HTTP request from your pipeline and Unhandled records a deployment: who shipped which version of which service, when it started, and how it ended. Charts across the app then show a marker at that moment, so a latency jump or a new issue lines up with the release that caused it.

The unhandled deploy command calls this endpoint for you and fills most fields from your CI environment. It needs Node 24 or later. Call the API yourself from other runtimes, or when you want to control every field. Deployments explains how declared deployments relate to the ones Unhandled detects from telemetry.

Declared and detected deployments share one list.

Authentication

Every request carries a deploy key in the x-api-key header. An admin creates deploy keys for a project under Settings > Deploy keys. A key starts with dk_, appears once when you create it, and can be revoked from the same page. The key decides the project, so the URL never names one.

POST/api/v1/deploymentsDeclare a deployment

The examples use https://unhandled.net, the CLI's default base URL. A self-hosted instance serves the same path on its own URL.

Headers

x-api-keystringrequired

A deploy key that starts with dk_.

content-typestringrequired

application/json.

x-unhandled-sourcestring

Send cli to record the deployment with the source cli. Any other value, or no header, records api. The CLI sets it for you.

Body

servicestringrequired

The service's telemetry name, the service.name it reports. 1 to 120 characters. A name Unhandled has not seen yet is accepted and stored as sent, so you can declare a deployment before the service sends any telemetry.

environmentstringrequired

production, staging or development.

versionstring

1 to 255 characters. Send version, commitSha, or both.

commitShastring

7 to 64 hexadecimal characters: 40 for SHA-1 repositories, 64 for SHA-256. Unhandled stores it in lowercase. A shorter SHA is expanded when it matches exactly one commit already recorded on the project's deployments, and stored as sent when it matches none. When it matches several, the request fails with 422 and the message tells you how many characters to send.

statusstringdefault succeeded

started, succeeded, failed or canceled.

startedAtstring

ISO 8601 timestamp. Defaults to the time Unhandled receives the request.

finishedAtstring

ISO 8601 timestamp. Defaults to the receive time, or stays empty while status is started.

externalIdstring

1 to 255 characters. Your pipeline's ID for this rollout. Send it to update the same deployment later.

actorstring

Up to 255 characters. The person or bot that triggered the deployment.

ciUrlstring

Up to 2048 characters. A URL with a protocol, linked from the deployment page.

repositoryUrlstring

Up to 2048 characters. A URL with a protocol. Unhandled uses it to link the commit.

metadataobject

Any JSON object up to 4 KB once serialized.

Request

curl -X POST "https://unhandled.net/api/v1/deployments" \
  -H "content-type: application/json" \
  -H "x-api-key: $UNHANDLED_API_KEY" \
  -d '{
    "service": "shop-api",
    "environment": "production",
    "version": "2026.10.21-1",
    "commitSha": "3f9c2a7e5b1d4c8f0a6e2d9b7c4a1f3e5d8b0c2a",
    "status": "started",
    "externalId": "release-4812",
    "actor": "release-bot",
    "ciUrl": "https://ci.example.com/pipelines/4812",
    "repositoryUrl": "https://github.com/example/shop",
    "metadata": { "region": "eu-west", "ordersMigration": true }
  }'

Response

{
  "id": "0b4f7d3e-2c1a-4e8b-9a6f-5d2c7e1b3a90",
  "source": "api",
  "status": "started",
  "merged": false,
  "url": "https://unhandled.example.com/o/acme/projects/shop/deployments/0b4f7d3e-2c1a-4e8b-9a6f-5d2c7e1b3a90"
}

url opens the deployment in the Unhandled app. merged is true when the declaration joined a deployment Unhandled had already detected from telemetry.

Report a rollout in two calls

A request without externalId always creates a new deployment. With one, Unhandled first looks for a deployment in the project with the same externalId and updates it. That lets a pipeline open the deployment before the rollout and close it afterwards.

  1. Open the deployment

    Send "status": "started" with an externalId, as in the request above. The deployment appears with no finish time.

  2. Close it with the outcome

    Send the same service, environment and externalId with the final status.

    Terminal
    curl -X POST "https://unhandled.net/api/v1/deployments" \
      -H "content-type: application/json" \
      -H "x-api-key: $UNHANDLED_API_KEY" \
      -d '{
        "service": "shop-api",
        "environment": "production",
        "version": "2026.10.21-1",
        "externalId": "release-4812",
        "status": "succeeded"
      }'

    Unhandled answers 200 with the updated deployment.

On an update:

  • Fields you leave out keep their previous values.
  • startedAt keeps the value from the first request. A later value is ignored.
  • finishedAt is set when the deployment reaches succeeded, failed or canceled, unless you send it.

Allowed status changes

FromTo
startedsucceeded, failed or canceled
Any statusThe same status again

Any other change, such as succeeded back to started, returns 409. Reusing an externalId that belongs to a deployment of another service or environment also returns 409.

Merging with detected deployments

Unhandled also detects deployments when a service starts reporting a new version in its spans. A declaration joins a detected deployment of the same service and environment when both hold:

  • The detected version was first seen within 60 minutes of startedAt, before or after.
  • The version matches, or the commitSha matches.

A declaration that only carries a commit therefore links to the release whose spans reported that commit in vcs.ref.head.revision. A merged declaration returns 201 with "merged": true.

Status codes

CodeWhen
201Unhandled created a deployment, or merged the declaration into a detected one.
200Unhandled updated the deployment with the same externalId.
401The deploy key is missing, unknown or revoked, or the request sent a DSN key.
403The project's organization is suspended.
409The status change is not allowed, or the externalId belongs to another service or environment.
422Validation failed, the environment is unknown, or a short commit SHA matches several commits.
429The key sent more than 60 requests in the current minute.

A validation failure returns an errors array whose entries carry a field and a message. An unknown environment returns a message and an environments array with the valid values. Every other error returns a message.

Rate limit

Each deploy key can send 60 requests per minute. The 61st request in that minute returns 429 with a retry-after header in seconds. The budget belongs to the key, so pipelines with separate keys never slow each other down.

Chart markers

Deployments with status failed or canceled stay in the history but get no marker on charts.

FAQ