# API changes: June 26, 2026

[← Changelog](/docs/changelog) · [June 2026](/docs/changelog/2026-06)

### FIX-0626-1: catalog-prices: system fields priceScale, extraId, timestampX declared in the schema

**Before**

[GET /v1/catalog-prices](/docs/entities/catalog-prices/list) and [GET /v1/catalog-prices/:id](/docs/entities/catalog-prices/get) returned the `priceScale`, `extraId`, and `timestampX` fields, but they were not declared in the schema: they passed through without normalization (the `timestampX` field arrived in an offset format, e.g. `2024-06-17T16:53:24+05:30`) and were absent from the [GET /v1/catalog-prices/fields](/docs/entities/catalog-prices/fields) response.

**After**

The three fields are declared as read-only. They are now listed in [GET /v1/catalog-prices/fields](/docs/entities/catalog-prices/fields), and `timestampX` is normalized to ISO 8601 UTC (`2024-06-17T13:53:24.000Z`), consistent with the other datetime fields.

**Impact on integrators**

The instant in `timestampX` does not change — only its string representation does (UTC instead of a local offset). Clients that parse the value with a standard date parser keep working unchanged.

### FIX-0626-2: Server and app source download: the signed URL no longer returns 403

**Before**

`GET /v1/infra/servers/:id/sources/:versionId/download` and `GET /v1/apps/:id/sources/:versionId/download` returned `200` with a signed URL, but downloading from that URL failed with `403 AccessDenied` when the request used a personal key (`vibe_api_*`). Listing versions worked and the file was physically present in storage.

**After**

The signed URL is now bound to the object's actual storage location, so the download returns the archive content. The fix also covers snapshots whose storage key belongs to a different family (legacy app snapshots linked to a server).

**Impact on integrators**

The endpoint contract is unchanged — this restores the documented `200` + working signed URL behavior. No client-side changes are required.

### NEW-0626-3: api-bearer token refresh and Gateway rejection reason

The new endpoint [POST /v1/infra/servers/:id/access-tokens/:tokenId/refresh](/docs/infra/access-tokens/refresh) mints a fresh JWT (up to 10 minutes) for an existing `api-bearer` token — without creating a new record, and without consuming the active-token limit or the hourly mint limit. A long-running client (CI, AI agent) refreshes the token before `jwtExpiresAt` instead of minting a new one. A single revoke kills the original and all refreshed JWTs of one token.

The `401 BH_LOGIN_REQUIRED` response that the Gateway returns on an app subdomain when it rejects the `Authorization: Bearer` header now carries a `reason` field with the specific cause: `expired`, `signature`, `subdomain`, `type`, `revoked`, `malformed`, or `invalid`. The field is additive — existing clients ignore it.
