Project archive API
Every project carries an archivedAt timestamp (epoch ms, or null for an active project) — a
reversible lifecycle flag, not a soft-delete. {projectId} is the project code (for example
ACME) everywhere below.
Archive makes a project’s content read-only while its administration stays reachable:
- Every existing route stays fully readable: an archived project’s detail page, suites, cases,
runs, plans, milestones, defects, attachments, comments and exports all keep working exactly as
they do for an active project.
GET /api/v1/projects/{projectId}still returns200, with a non-nullarchivedAt. - Every write to content is refused
409 { code: "project_archived" }— see Content writes are refused while archived below. - It does not release a project seat. An organization at its
projectLimitthat archives a project is still refused a new one with409 { code: "project_limit_exceeded" }— deleting remains the only way to free a seat.
Breaking change: the list defaults to active only
Section titled “Breaking change: the list defaults to active only”GET /api/v1/projects now hides archived projects by default. This is a deliberate change to a
shipped public contract: an integrator polling this endpoint starts receiving fewer rows once anything is
archived in that organization. It is observationally inert on the day this ships, because no project can
be archived until this capability lands — but it takes effect immediately once you (or anyone in your
organization) archives a project.
Pass the optional status query parameter to change what the list returns:
status value | Result |
|---|---|
| absent, empty, or all-unknown tokens | Active only — the new default |
active | Active only (same as the default, spelled out) |
archived | Archived only |
active,archived (either order) | Both — the full pre-existing behavior |
status is a comma-separated token set, matching the existing runs/defects/milestones/cases
filters on this endpoint. It composes with pagination, q, memberUlid, and every other filter, and the
returned total always agrees with the applied filter (never a phantom count from an unfiltered query).
# Only what changed since the flip: archived projectscurl -sS -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/projects?status=archived"
# Everything, exactly like before this change shippedcurl -sS -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/projects?status=active,archived"Every other consumer of the projects list inherits this same default with zero extra work on your
part: the web directory, the project switcher, and any other internal reader all resolve to active-only
unless they too pass status explicitly.
Archive a project
Section titled “Archive a project”/api/v1/projects/{projectId}/archiveNo request body. Returns 200 with the updated project (see
Project access for the sibling resource; this is the plain project
object with its archivedAt field).
{ "ulid": "01J...PROJECT", "name": "Acme", "id": "ACME", "description": null, "avatarKey": null, "avatarVersion": 0, "createdAt": 1700000000000, "updatedAt": 1700000005000, "archivedAt": 1700000005000}Idempotent: archiving an already-archived project responds 200 without changing the stored
timestamp — a double-click or a retried request behaves identically to a single call, and this endpoint
never answers 409.
Authorization
Section titled “Authorization”Same gate as PATCH/DELETE on a project: only an organization owner or admin may archive.
A member or viewer is refused 403 forbidden, with archivedAt left unchanged. An unknown
{projectId}, or one belonging to another organization, answers 404 not_found.
Unarchive a project
Section titled “Unarchive a project”/api/v1/projects/{projectId}/unarchiveNo request body. Returns 200 with the updated project, archivedAt: null. Same idempotency and
authorization rules as archiving, mirrored: unarchiving a project that is not archived succeeds and
leaves archivedAt as null.
Example
Section titled “Example”curl -sS -X POST \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/projects/ACME/archive"Content writes are refused while archived
Section titled “Content writes are refused while archived”An archived project’s content is frozen. Any write into it — creating a suite, updating a test case, starting a run, saving a run result, linking a defect, mutating a test plan, writing a custom-field value, creating a comment, uploading an attachment or an avatar, and every other project-scoped content mutation — is refused:
{ "error": { "code": "project_archived", "message": "project is archived" }}Status 409. This applies whether the write targets the project directly
(/api/v1/projects/{projectId}/...) or an entity reached by its own ULID
(/api/v1/test-cases/{ulid}, /api/v1/runs/{ulid}, and so on) — the refusal is enforced where the
data lives, not where the URL points, so there is no route that bypasses it. An attachment upload
refused this way never reaches storage: no partial or orphaned object is left behind. This applies
to every credential kind, including an organization API token.
Refusal ordering is fixed: a project that does not exist (or belongs to another organization)
answers 404 before archived state is ever consulted; a caller without access to a private
project answers 403; only a caller who would otherwise have been admitted reaches the 409. The
archived state never leaks the existence of a project the caller cannot see.
This is a breaking change to a shipped public contract, and unlike the list-default change above it is not observationally inert — any project archived before this change now refuses writes the moment it ships.
Exempt administrative operations
Section titled “Exempt administrative operations”These operations manage the project itself rather than its content, and keep succeeding while the project is archived:
| Operation | Route |
|---|---|
| Unarchive | POST /api/v1/projects/{projectId}/unarchive |
| Delete | DELETE /api/v1/projects/{projectId} |
| Archive (idempotent re-archive) | POST /api/v1/projects/{projectId}/archive |
| Update (rename/re-describe) | PATCH /api/v1/projects/{projectId} |
| Access management (mode, membership, ownership) | PATCH /api/v1/projects/{projectId}/access and its member/group sub-routes |
The project’s own avatar is content, not administration, and is not exempt:
POST/DELETE /api/v1/projects/{projectId}/avatar are refused 409 like any other write.
Idempotent retries
Section titled “Idempotent retries”A write refused 409 project_archived under an Idempotency-Key is recorded like any other
response below 500, so an identical retry within the key’s TTL replays the same recorded 409
— even after the project has since been unarchived. Send a fresh, unused key to get the
post-unarchive outcome. See Idempotency for the general contract.
Known limitation
Section titled “Known limitation”Hard delete (DELETE /api/v1/projects/{projectId}) is unrelated to archive and remains the only
irreversible removal path. Deleting does not reclaim object storage — the project’s picture and every
descendant step attachment survive the cascade. This is pre-existing, out of scope here, and tracked as
its own future change.