
# Publish a connection revision

`POST /v1/connections/{connection_id}/revisions/{revision_id}:publish` · permission `connections:write`

Makes a draft revision the live configuration of an enterprise connection. In one step the revision's material replaces the live one:

- SAML: the identity provider's entity ID, SSO and SLO URLs, and the revision's signing certificates;
- OIDC: the client fields and, if the revision carries one, the new client secret;
- SP host migration: the SP host and the SP URLs. The previous host keeps answering for 30 more days, then returns `404`.

The revision needs a **passed test sign-in of that revision** from the last 24 hours: start one with [Test a connection](test.md) and `revision_id`, and finish it in the browser. Testing the live configuration does not count.

Publishing a revision that is already published writes nothing and returns `200` with `already_published: true`; of two concurrent publications only one writes. The publication is audited as `connection.revision_published`.

There is no request body (send none or `{}`), and no `If-Match`: a revision has no version. If the live configuration changed after the draft was made, publication is refused and you make a new draft.

## Request

| Field | Type | Required | Description |
|---|---|---|---|
| `connection_id` | path | yes | Enterprise connection ID (`conn_…`). |
| `revision_id` | path | yes | Revision ID (`connrev_…`) from [List the revisions](revisions.md). |
| `Idempotency-Key` | header | no | Optional key that makes a retried request safe (16-64 characters). |

## Response

`200 OK`:

```json
{
  "revision": {
    "id": "connrev_01j9xr8c2d4e6f8g0h2j4k6m8n",
    "connection_id": "conn_01j9xp6q0r1s5t7v9w1x3y5z7b",
    "protocol": "saml",
    "kind": "sp_host",
    "status": "published",
    "discard_reason": null,
    "created_at": "2026-10-28T10:00:00Z",
    "decided_at": "2026-10-28T11:00:00Z",
    "purge_after": "2027-01-26T11:00:00Z",
    "superseded_revision_id": null,
    "changes": {
      "sp_host": "login.example.com",
      "idp_entity_id": null,
      "idp_sso_url": null,
      "idp_slo_url": null,
      "certificate_fingerprints": [],
      "mfa_authn_context_values": null,
      "issuer": null,
      "client_id": null,
      "client_auth_method": null,
      "provider_profile": null,
      "client_secret_change": null
    },
    "latest_test": {
      "test_id": "conntest_01j9xr4a6b8c0d2e4f6g8h0j2k",
      "status": "passed",
      "failure_reason": null,
      "started_at": "2026-10-28T10:05:00Z",
      "completed_at": "2026-10-28T10:06:00Z"
    }
  },
  "already_published": false
}
```

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `unknown_field` | The body is not empty and not `{}`. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The credential lacks `connections:write`. |
| 404 | `not_found` | The connection or the revision does not exist in this environment, or the revision belongs to another connection. |
| 409 | `conflict` | A precondition does not hold; `detail` says which: the revision was discarded; it has no passed test from the last 24 hours; the connection changed after the draft was made (make a new draft); the previous SP host migration's 30-day window is still open; the new SP host is no longer a host of this environment; the connection would exceed 3 active signing certificates. |
| 429 | `rate_limited` | The rate limit was exceeded; retry after the `Retry-After` period. |
| 503 | `service_unavailable` | A dependency is temporarily unavailable; retry after the `Retry-After` period. |

## Example

```bash
curl -X POST "https://api.eu.keenauth.com/v1/connections/conn_01j9xp6q0r1s5t7v9w1x3y5z7b/revisions/connrev_01j9xr8c2d4e6f8g0h2j4k6m8n:publish" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

```bash
keenauth connections revisions publish conn_01j9xp6q0r1s5t7v9w1x3y5z7b connrev_01j9xr8c2d4e6f8g0h2j4k6m8n --yes
```
