How to use the DORA API to send deployment activity to Span

Last updated: October 27, 2026

Span's DORA API lets your deploy pipeline and incident tooling send deployments, incidents, and services to Span. Span uses them for Deployments / week, Change failure rate, Mean time to recover, and lead time for changes.

Why it matters

Span has no connector that reads deployments from GitHub Actions, GitLab CI/CD, Azure DevOps, or other CI tools. The DORA API (or a $deploy event) is how deployments reach Span. The same API lets you:

  • Send incidents from tools Span doesn't connect to, such as PagerDuty or Opsgenie, or record failures your team fixes with a hotfix and never opens an incident for.

  • Define services and their owning teams so deployments count toward the right team.

How it works

All endpoints live under https://span.app/api/dora/v1 and take and return JSON.

Resource

Endpoints

Deployments

POST /deployments, GET /deployments, GET /deployments/:id, PATCH /deployments/:id, DELETE /deployments/:id

Incidents

POST /incidents, GET /incidents, GET /incidents/:id, PATCH /incidents/:id, DELETE /incidents/:id

Services

POST /services, GET /services, GET /services/:id, PATCH /services/:id, DELETE /services/:id

Authentication. Send Authorization: Bearer <token> on every request. Two kinds of token work:

  • Service account token (recommended for CI): in Span, go to Settings → Service Accounts, open the menu on the service account's row, choose Manage Token, then Generate. Only people who can manage service accounts see this page. A service account token keeps working when people change roles or leave.

  • Personal access token: from Settings → Access Token. It acts as the person who created it.

A missing or invalid token returns 401 Unauthorized. Store tokens as CI secrets, never in workflow files.

Request headers.

  • X-Request-ID: optional. If you don't set it, Span generates one. Every response includes the request ID in this header.

  • Idempotency-Key: optional, on POST and PATCH. Span processes a request with a given key once in 24 hours. A repeat with the same key, token, method, and path returns the original response with X-Replayed-Request: true. Span saves only successful responses, so a failed request can be retried with the same key. Other methods ignore the key.

Timestamps must be ISO-8601 in UTC and end in Z, such as 2026-03-25T18:00:00Z. Offsets like +00:00 return a 400 error.

Pagination. List endpoints use cursor pagination with before, after, and limit query parameters. limit defaults to 10, with a minimum of 1 and a maximum of 100; larger values are rejected. Responses include meta.page with startCursor, endCursor, hasNextPage, and hasPreviousPage. Pass endCursor as after to get the next page.

Deployments

Field

Required

Description

title

Yes

A short name for the deployment.

triggeredAt

Yes

When the deployment started. Span dates the deployment by this time.

completedAt

No

When the deployment finished. Lead time ends at completion, so send it.

status

No

success, failure, or pending. Defaults to pending, which never counts in metrics.

type

No

deploy, rollback, or restart. Defaults to deploy. All three count.

environment

No

For example production or staging. Leave it out for production.

description, version, httpUrl

No

Extra context. httpUrl must be a valid URL.

services

No

Service slugs. Span credits the deployment to the services' owning teams. A slug Span hasn't seen creates a new service with no owner.

deployer

No

name and email of the person who deployed, both required if you send it. Stored for reference only; Span doesn't use it for team attribution.

git

No

repoUrl and refName, both required if you send it. Use the full commit SHA as refName.

pullRequests

No

PR numbers shipped in this deployment, for monorepos or partial deploys. Needs a GitHub (github.com or *.ghe.com) or Azure DevOps git.repoUrl, or the call returns a 400 error. Used only when deploy-to-PR linking is on for your organization.

metadata

No

An object of extra key-value pairs. Values must be strings.

curl -X POST https://span.app/api/dora/v1/deployments \
  -H "Authorization: Bearer $SPAN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: deploy-12345" \
  -d '{
        "title": "billing-api 2fd4e1c",
        "triggeredAt": "2026-03-25T18:00:00Z",
        "completedAt": "2026-03-25T18:10:00Z",
        "type": "deploy",
        "status": "success",
        "environment": "production",
        "version": "2fd4e1c",
        "services": ["billing-api"],
        "deployer": {"name": "First Last", "email": "first.last@example.com"},
        "git": {
          "repoUrl": "https://github.com/example/billing-api",
          "refName": "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
        },
        "pullRequests": [101, 102, 103],
        "metadata": {"pipeline": "release"}
      }'

A successful call returns the deployment in data, including its id, with a meta.cursor. GET /deployments/:id returns the same shape, and GET /deployments returns a page of them.

Update a deployment with PATCH /deployments/:id. Every field is optional. Fields you leave out stay unchanged, and null clears a field that allows it. metadata is merged: keys you send are added or updated, and keys you leave out stay. A common pattern is to post with status: "pending" when a deploy starts, then PATCH it to success or failure with completedAt when it ends.

Delete a deployment with DELETE /deployments/:id. Span returns 204 No Content.

Incidents

Field

Required

Description

title

Yes

A short name for the incident.

severity

Yes

A whole number, 0 or higher, using your own severity scale. Span stores it; no DORA metric filters on it.

issuedAt

Yes

When the incident was declared. Span places the incident in a date range by this time.

startedAt

No, but send it

When the incident started. Mean time to recover runs from here. Without it, the incident counts as 0 in MTTR.

endedAt

No

When the incident was resolved. Until it's set, the incident counts as 0 in MTTR.

environment

No

Total incidents and Change failure rate count an incident when this is blank or contains "prod" or "prd". MTTR counts every environment.

owners

No

Teams that own the incident, as [{"type": "team", "slug": "team-a"}]. Only team is supported, and the slug must match an existing Span team. Without owners, the incident counts in your organization total only.

services

No

Service slugs affected by the incident.

description, httpUrl, git

No

Extra context. git takes repoUrl and refName.

triggeringDeployments, resolvingDeployments

No

IDs of deployments that caused or resolved the incident. Span stores them; Change failure rate doesn't use them today.

metadata

No

An object of extra key-value pairs. Values must be strings.

curl -X POST https://span.app/api/dora/v1/incidents \
  -H "Authorization: Bearer $SPAN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Checkout errors",
        "severity": 1,
        "issuedAt": "2026-03-25T18:00:00Z",
        "startedAt": "2026-03-25T17:45:00Z",
        "environment": "production",
        "owners": [{"type": "team", "slug": "payments"}]
      }'

Update an incident with PATCH /incidents/:id, for example to send {"endedAt": "2026-03-25T19:00:00Z", "owners": [{"type": "team", "slug": "payments"}]} when it resolves. Fields you leave out stay unchanged, except owners: a PATCH without owners removes the incident's owning teams, so always send the full owners list. null clears a field that allows it. Delete one with DELETE /incidents/:id (204 No Content).

Services

Field

Required

Description

name

Yes

A display name for the service.

slug

No

The value you send in a deployment's services. Lowercase, starting with a letter, with letters and numbers optionally separated by single - or _ characters, such as billing-api. If you leave it out, Span builds one from name. Slugs must be unique; creating a service with an existing slug returns an error.

owners

No

Owning teams, as [{"type": "team", "slug": "team-a"}]. The slug must match an existing Span team.

description, git.repoUrl, metadata

No

Extra context. metadata values must be strings. On update, keys you send are added or updated, and other keys stay.

curl -X POST https://span.app/api/dora/v1/services \
  -H "Authorization: Bearer $SPAN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Billing API", "slug": "billing-api", "owners": [{"type": "team", "slug": "payments"}]}'

Update a service with PATCH /services/:id and delete one with DELETE /services/:id. Always include the full owners list when you PATCH a service; a PATCH without owners removes the service's owning teams.

Ownership: connecting deployments and incidents to teams

Deployments, by default: Span credits each deployment to the teams that own the services in its services list. The API is the only way to set service owners; there is no Catalog screen for it. To give an existing or auto-created service an owner, find its id with GET /services (match on slug), then:

curl -X PATCH https://span.app/api/dora/v1/services/<serviceId> \
  -H "Authorization: Bearer $SPAN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"owners": [{"type": "team", "slug": "team-a"}]}'

Deployments, with deploy-to-PR linking on: Span turns this on for your organization on request. Span then links each deployment to its PRs (from pullRequests, or inferred from git.refName) and credits the teams of those PRs' authors instead of service owners. A deployment Span can't place (no linked PRs, or authors who aren't on a team) counts in your organization total only.

Incidents: Span credits each incident to the teams in its owners list.

A deployment or incident with no team still counts in your organization total. "Deployments in the Catalog but not under my team" almost always means the service has no owner.

Setup

  1. Create a service account token in Settings → Service Accounts and store it as a CI secret.

  2. Create each service once with its owning team (POST /services).

  3. Add a step to your production deploy pipeline that sends POST /deployments with status: "success", services, and git with the full commit SHA. Add an Idempotency-Key so retries don't create duplicates.

  4. If your incidents don't come from incident.io or Rootly, send them with POST /incidents, including startedAt, owners, and later endedAt.

  5. Check Catalog → Deployments and Catalog → Incidents, then the DORA page under Insights → Productivity → DORA.

Things to know

  • Status defaults to pending. Only success deployments count. A deployment sent without a status never counts until you PATCH it.

  • Production means blank or containing "prod" or "prd". Matching ignores case and checks for the substring, so production, PRD-us-east-1, and pre-prod count, while staging, dev, test, and qa don't. Avoid those substrings in names you don't want counted.

  • Without an Idempotency-Key, every POST creates a new record. A re-run step or automatic retry then counts the same deployment twice. Delete extras with DELETE /deployments/:id.

  • Deploy-to-PR linking has requirements. Span must turn it on for your organization. The repository in git.repoUrl must be one Span already syncs from your git connection, using a https://github.com/owner/repo, https://<name>.ghe.com/owner/repo, or https://dev.azure.com/org/project/_git/repo URL. Inferred linking matches the full commit SHA in git.refName against PR merge commits. PRs that haven't synced yet link once they arrive.

  • The deployer field doesn't decide team credit. Services or linked PR authors do.

  • Team totals can exceed the organization total. A deployment or incident with two owning teams counts once for each team and once for the organization. With deploy-to-PR linking on, a deployment that ships PRs from authors on two teams also counts for both. Don't add team rows together.

  • Processing time. Records appear in the Catalog as soon as the call succeeds. PR linking and lead time are calculated in the background; expect them on the DORA page within about 3 hours.

  • Change failure rate is incidents ÷ deployments. Span doesn't trace each incident to the deployment that caused it, so treat CFR as a trend indicator.

  • Worked example. In March you send 40 successful production deployments for billing-api, owned by the payments team, and 2 production incidents with owners set to payments, each with startedAt and endedAt 30 and 90 minutes apart. For March, payments shows about 9.0 deployments per week (40 ÷ 31 days × 7), a change failure rate of 5% (2 ÷ 40), and an average MTTR of 60 minutes.

What each DORA metric needs:

Metric

Needs

Deployments / week

Production deployments with status: "success"

Lead time for changes

Production deployments linked to PRs, which needs Span to turn on deploy-to-PR linking for your organization

Change failure rate

Production deployments and production incidents in the same range

Mean time to recover

Incidents with a start and resolved time (startedAt and endedAt on the API)

Incidents alone (for example from incident.io) power incident counts and MTTR. Deployments / week, lead time, and change failure rate also need deployments sent through this API or a $deploy event.

Related articles

Send deployments to Span from GitHub Actions or AWS CodePipeline

Lead Time to Production in Span

Deployments / Week

Change Failure Rate Report

Mean Time To Recover (MTTR) Report

Need help?

Contact your Span account team or reach out to support@span.app.