> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pool.day/llms.txt
> Use this file to discover all available pages before exploring further.

# Keys, apps and run reports

> Test and live keys, rotating a key without breaking shipped builds, turning an app off, and the indexing run reports you can see in the dashboard.

## Test and live keys

Every app gets a test key (`pk_test_…`) and a live key (`pk_live_…`) in the [dashboard](https://pool.day/developers/dashboard). A key only works from the bundle IDs you list for its app.

PoolKit is in early access. Anyone can sign in to the dashboard, but an account makes apps and keys only after a demo with us; [book one](https://pool.day/developers). Until then, creating an app answers `accessPending`, in the dashboard and through the [MCP server](https://docs.pool.day/mcp.md).

* Test keys are for development: they never count toward billing, have small allowances, and keep working in the Simulator when [App Attest](https://docs.pool.day/app-attest.md) is enforced.
* Live keys go in the builds you ship.

Pool keeps only a fingerprint of each key, so a key is shown once, when it is made.

## Rotate a key

Choose **Rotate** next to a key. You get a new key of the same kind and choose what happens to the old one:

| Choice | Old key |
| - | - |
| Keep old key 30 days | Keeps working for 30 days, so builds already in people's hands have time to update. The dashboard shows the date it stops. |
| Stop old key now | Stops at once, as if revoked. Use it when a key leaked. |

While a key is in its 30 days, `POST /sdk/v1/installs` says when it stops, and PoolKit logs a warning so you notice before it does.

**Revoke** stops a key at once without making a new one.

## Turn an app off

**Turn app off** refuses every key of the app until you turn it back on, without revoking any. Cloud assist calls answer 403 with `app_disabled`. PoolKit keeps working on the device: search, indexing and everything that never needed the service.

## Apps waiting for approval

A bundle ID belongs to the first app that lists it. When another developer's app already lists one of yours, your app waits for Pool to check who owns it, and its keys answer 403 with `app_pending` until then. Write to [hey@pool.day](mailto:hey@pool.day) with a way to show the app is yours, such as an App Store Connect screenshot. Listing a bundle ID in several of your own apps is fine.

## When a key stops working

PoolKit checks the key when the client opens, before indexing (at most every 15 minutes) and when the app comes to the foreground (at most daily).

| The service answers | What PoolKit does |
| - | - |
| 401 `invalid_key` (revoked, unknown, or past its rotation) | Locks the key: every call throws `PoolError.invalidAppKey` and indexing stops, until the app opens with a working key. |
| 403 `app_disabled`, `app_pending` or `bundle_not_allowed` | Turns cloud assist off: cloud calls throw `invalidAppKey`. Everything on the device keeps working, and cloud assist comes back once the app is allowed again. |
| 429, 5xx or no network | Nothing changes. PoolKit tries again later. |

While a key is in its rotation grace, PoolKit logs a warning with the date it stops (subsystem `day.pool.kit`, category `key`), so you see it in Xcode before builds break. PoolKit never treats a network failure as a dead key.

After 30 days locked in a row, PoolKit deletes the local index to keep nothing it can no longer use. Grants and downloaded models stay, so a working key rebuilds the index without asking the person again.

## Run reports

Turn on `sendsRunReports` in your index options and PoolKit sends a short report after each indexing run. The dashboard shows the last 30 days per app: how many runs, how many installs, how many completed, the median and slowest times, photos read per second, the commonest error codes and the newest runs.

A report holds only:

| Field | Example |
| - | - |
| Kind of run | `first`, `update`, `background`, `rebuild` |
| How it ended | `completed`, `stopped`, `endedByIOS`, `failed`, `interrupted`, `waiting`, with an error code such as `storage` |
| When and how long | Start time, duration, milliseconds per step |
| Counts | Photos in scope, indexed, skipped, failed |
| The device | Peak memory, iOS version, hardware model such as `iPhone17,1`, SDK version |

No text, labels, places, file names or photo identifiers, and the service refuses a report with any other field. Reports are kept 90 days. They cost nothing and are off by default.

### Reading a report

| Kind | Meaning |
| - | - |
| `first` | The index was empty when the run started. |
| `update` | Any other run in the app. |
| `background` | Started by a background task, registered with `registerBackgroundTasks` or `registerBackgroundIndexing`. |
| `rebuild` | The run after `rebuildIndex()`. |

| Outcome | Meaning |
| - | - |
| `completed` | Read everything in scope. |
| `stopped` | The app paused indexing. |
| `endedByIOS` | iOS cut a background run short. The next run carries on. |
| `interrupted` | Cancelled in the foreground, for example when the app closed. |
| `waiting` | Nothing to do yet: the error code is `grantNotFound` (no active grant) or `permissionDenied` (no photo access). |
| `failed` | Stopped on an error. The error code is a [PoolError](https://docs.pool.day/errors.md) case, `storage` when the error came from iOS itself. |

Counts: `total` is the captures known after the run, `indexed` those read in it, `skipped` those held back as sensitive or outside a food focus, and `failed` those Photos gave no image for. Timings are in milliseconds for the steps that ran: `readingLibrary`, `indexing`, `visual`, `language`, `places` and `people`.

PoolKit sends a report at most twice, never on a metered network unless `allowsCellular` is on, and never once the key has been refused.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.