# Migration guide: paginated lists and record-only deletion

This guide accompanies the API pagination and deletion update. It does not announce a deployment date. Confirm rollout availability before switching a production integration.

## Action required for existing list callers

GET `/api/v1/posts`, `/api/v1/videos`, and `/api/v1/stories` return **one page**, not every record. The existing `data.posts`, `data.videos`, and `data.stories` arrays remain, but a single request is no longer a complete export.

- Supply an owned `contentGroupId` on every request.
- `limit` defaults to **50**, with a maximum of **200**.
- Pass `data.pagination.nextCursor` as `cursor` for the next request. A cursor is an opaque continuation token: use it unchanged rather than constructing one.
- Stop when `nextCursor` is `null`, not when the array has fewer than your requested limit.
- Keep the same API account, resource, content group, and status filter throughout a traversal. Changing them invalidates the cursor. Page size may change.
- `offset` is not supported and returns `400`. Invalid limits, malformed or mismatched cursors, and duplicate supported parameters also return `400`.
- Pages reflect live data, not a frozen snapshot. New records ahead of the cursor require a fresh traversal; status changes and deletion can affect later pages.

### Server-side JavaScript example

Keep API keys on your server, never in browser code. This example reads records only; it does not delete or publish anything. Set `resource` to `posts`, `videos`, or `stories`.

```js
async function* listPages(resource, contentGroupId, apiKey, status) {
  if (!['posts', 'videos', 'stories'].includes(resource)) {
    throw new Error('Unsupported resource');
  }
  let cursor = null;
  do {
    const query = new URLSearchParams({ contentGroupId, limit: '50' });
    if (status) query.set('status', status);
    if (cursor) query.set('cursor', cursor);
    const response = await fetch(
      `https://www.postquick.ai/api/v1/${resource}?${query}`,
      { headers: { Authorization: `Bearer ${apiKey}` } },
    );
    if (!response.ok) throw new Error(`List failed: ${response.status}`);
    const { data } = await response.json();
    // Fail visibly on an older API response instead of silently exporting one page.
    if (!data?.pagination || !Array.isArray(data[resource])) {
      throw new Error('Pagination response unavailable; confirm API rollout');
    }
    yield data[resource];
    cursor = data.pagination.nextCursor;
  } while (cursor !== null);
}

for await (const posts of listPages('posts', contentGroupId, apiKey)) {
  await processPage(posts); // Your server-side export or processing function.
}
```

## What DELETE does—and does not do

Existing single-record DELETE endpoints remove the owned PQAI record and its future schedule before a publisher claims it. They **retain hosted media**, do **not reclaim storage**, do **not delete published social-platform posts**, and do **not refund credits**. Save any platform IDs needed for later administration before deleting the local record.

Successful responses retain `deleted: true` and add explicit guarantees:

```json
{"success":true,"data":{"deleted":true,"mediaRetained":true,"platformPostsDeleted":false}}
```

Active publishing/generation, unknown states, or a concurrent change can return `409`. Pending video generation is also blocked. Refresh the record and reconcile any remote publishing outcome before retrying; deletion is not cancellation of an in-flight job. Missing or unowned records return `404`, including a second deletion of an already removed record.

Do not use record deletion as a storage-cleanup workflow. Shared files can still be required by other posts or stories. The original pagination/deletion update did not include bulk deletion or automatic media reclamation. The separate follow-up release below adds bounded bulk deletion only; it does not change these pagination migration steps.

## Follow-up release: bulk delete selected IDs

A separate follow-up release adds `POST /api/v1/posts/bulk-delete`, `POST /api/v1/videos/bulk-delete`, and `POST /api/v1/stories/bulk-delete`. Requires the bulk-deletion API update. Confirm rollout availability before use. Existing single-record DELETE paths remain supported.

Send `{ "ids": ["11111111-1111-4111-8111-111111111111"] }`: 1–50 unique UUIDs and no extra fields. Invalid bodies are rejected with 400 before any writes. Use only explicitly selected, confirmed IDs and server-side API keys—do not turn the pagination example into a delete-all loop. Each item requires ownership of its content group and follows the single-record deletion safety rules.

HTTP 200 means the batch was processed, even if some or all items failed. This is not all-or-nothing: successful items are not rolled back. Inspect `data.results` and `data.summary` (`requested`, `deleted`, `failed`):

- Success: `{id, deleted:true, status:200, mediaRetained:true, platformPostsDeleted:false}`.
- Failure: `{id, deleted:false, status:404|409|500, error:{code,message}}`.
- Missing and unowned records share 404. Unsafe states or concurrent changes return 409. Database failures return item-level 500 and may have ambiguous outcomes: refresh and reconcile before retrying.
- Authentication and other request-level errors keep normal API behavior, rather than returning batch results. Never automatically retry a batch or a network failure.

Bulk deletion does not reclaim storage, remove hosted/external files or social-platform posts, cancel in-flight jobs, or refund credits/usage. See the [bulk deletion reference and server-side JavaScript example](/developers#bulk-delete).

## Before rollout

1. Test a group containing more than 50 records and verify all pages are processed.
2. Confirm existing callers no longer assume one request returns everything.
3. Handle `400`, `404`, and `409` explicitly; do not blindly retry a failed publish.
4. Coordinate deployment of the API update and client changes. The example intentionally fails if pagination metadata is unavailable.

See the [API reference](/developers) and [OpenAPI specification](/openapi.json) for endpoint schemas.
