# Snapshot before publishing

Publishing an application requires a saved source snapshot. This page covers what exactly is checked, what arrives in the `409 SNAPSHOT_REQUIRED` refusal, and how to publish sources that the deploy auto-save stored on a server rather than on the application.

Storage overview and the endpoint reference — [Source code storage](/docs/source-storage).

## Snapshot presence guarantee before publishing

`POST /v1/apps/:id/publish` checks that the app has a saved source snapshot. The snapshot's age does not affect the outcome — a version saved long ago is accepted just as readily as one created a moment ago. The `409 SNAPSHOT_REQUIRED` refusal arrives when there is no usable snapshot: none was ever saved, or the files of the saved version have been deleted. The hint says which call to make before publishing again.

The check works only if source saving is enabled in the Bitrix24 account (enabled by default; the Bitrix24 account owner can disable it — [Disabling for a Bitrix24 account](/docs/source-storage/registry#disabling-for-a-bitrix24-account)).

Refusal example:

```json
{
  "success": false,
  "error": {
    "code": "SNAPSHOT_REQUIRED",
    "message": "Publish requires a saved source snapshot for this app. Call POST /v1/apps/:id/sources, or publish with sourceServerId set to the server you deployed to — a deploy auto-save is stored on that server, not on the app.",
    "hint": {
      "requiredAction": "POST /v1/apps/:id/sources",
      "reason": "app_snapshot_missing",
      "serverKeyedSources": {
        "requiredAction": "POST /v1/apps/:id/publish with sourceServerId (the server you deployed to)",
        "bodyField": "sourceServerId",
        "header": "x-source-server",
        "listEndpoint": "GET /v1/infra/servers/:serverId/sources",
        "registryEndpoint": "GET /v1/me/sources"
      },
      "toolName": "save_sources",
      "lastSnapshot": null
    }
  }
}
```

The `hint.lastSnapshot` block is non-empty only when the platform checks the snapshot's age. Inside it, `timestamp` is when the version was created, `presentedAt` is when it was last presented by a save, and `ageMinutes` is counted from `presentedAt`: on a deduplicated save the two values diverge. The `hint.freshnessWindowMinutes` field arrives on the same condition — it is the length of the freshness window in minutes.

The `hint.lastSnapshot` field is `null` when there is no usable snapshot — none was saved, or the files of the saved version have been deleted. While the snapshot's age is not checked, that is the only cause of this refusal, so in that refusal the field is always `null`.

`hint.reason` names the refusal cause with one of two values: `app_snapshot_missing`, `server_snapshot_missing`. Two further values — `app_snapshot_stale` and `server_snapshot_stale` — arrive only when the platform checks the snapshot's age. The `hint.serverKeyedSources` block arrives only when the request named no server — it explains what to do when the sources were stored by a deploy auto-save.

### Publishing sources from a server

The deploy auto-save stores sources on the server you deployed to. If that server does not belong to the OAuth key of the app you are publishing — for example, the deploy ran under a personal `vibe_api_` key — the snapshot is kept on the server (its `appId` is empty), and the publish check, which looks at the app's snapshots by default, does not find it. The deploy answers `autoSaved: true` truthfully: the bytes are saved.

To publish exactly those sources, name the server:

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/apps/$APP_ID/publish" \
  -H "X-API-Key: $VIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sourceServerId": "'"$SERVER_ID"'"}'
```

The `X-Source-Server: <serverId>` header means the same.

What matters here:

- **The platform never picks the server.** You pass the identifier — the same `:id` you called deploy with. Auto-picking "the nearest" server would publish sources nobody named.
- **Rights to the server are checked separately from rights to the app.** A server's sources can be published by the server-owner key, a personal key of the same user, or a Bitrix24 account administrator. No such server, a server from another Bitrix24 account, or a deleted one — `404 SERVER_NOT_FOUND`; no rights — `403 NOT_AUTHORIZED`.
- **A server has its own version numbering.** Together with `sourceServerId`, the `sourceVersionId` field means a version of THAT server: `v3` of the server and `v3` of the app are different versions.
- **Publication does not mark a server's version.** For the app's own version, publication adds the `published` tag and writes a publication stamp; for a server's version it does neither. On a server version those same fields hold the DEPLOY history (which is what `GET /v1/infra/servers/:serverId/sources` shows), and the `published` tag means indefinite retention plus a delete block. Publishing one app changes neither of them on a version that does not belong to it. What was published is visible in the audit log as the publication record.
- **A published server version has no indefinite retention.** With no tag on it, it follows the ordinary cleanup rules: the platform always keeps the five most recent versions, plus one version per day for the last 14 days and one per week for the last 4 weeks. New deploys to the same server push the published version out of those rules, and it can be removed while the app stays published. A `warnings` line in the publish response says the same. To keep the version forever, tag it yourself — that is your call, because indefinite retention takes up space and blocks deletion:

  ```bash
  curl -X PATCH "https://vibecode.bitrix24.com/v1/infra/servers/$SERVER_ID/sources/v3" \
    -H "X-API-Key: $VIBE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"tags": ["manual"]}'
  ```

  Tags are replaced wholesale: if the version already carries labels of its own, list them in the same array next to `manual`. The ready-made list arrives in that same `warnings` line.
- **Without `sourceServerId` everything works as before** — the check looks for a saved snapshot of the app, then tags it and writes the publication stamp.
- **The header is validated like the field — but only when the field is absent.** If `sourceServerId` came in the body, the body decides: the header is not read, and whatever it holds has no effect on the answer. With no field in the body the header is validated strictly: a header that is not in UUID form, or is sent twice, gives `400 VALIDATION_ERROR`. It must not be silently ignored in that case — otherwise publication would take the app's snapshot while you believe you named a server.
- **Source storage disabled for the Bitrix24 account** — the selector is not used at all, publication proceeds, and `warnings` carries a line saying the `sourceServerId` you passed went unused.

Where to find the server identifier if you did not keep it: `GET /v1/infra/servers/:serverId/sources` lists the versions of one server, and `GET /v1/me/sources` is the aggregated registry of every source the key can reach, by server and by app.

Parameters for re-publishing the same version (not the most recent one):

- `sourceVersionId` in the body — format `v<N>`, for example `"sourceVersionId": "v3"`.
- The `x-source-version: v3` header — an alternative to the body.

## Error codes

`POST /v1/apps/:id/publish` refusals specific to the snapshot check:

| HTTP | Code | When returned |
|------|-----|---------------------|
| 409 | `SNAPSHOT_REQUIRED` | There is no saved snapshot, or the files of the saved version have been deleted (only when saving is enabled). The response includes a `hint` field describing the action. |

The full code reference — [Error codes](/docs/errors).

## See also

- [Source code storage](/docs/source-storage)
- [Automatic save on deploy](/docs/source-storage/auto-save)
- [Save a snapshot](/docs/source-storage/save)
- [Version lifetime and cleanup](/docs/source-storage/retention)
- [Publishing an app](/docs/apps/publish)
