> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.coperniq.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.coperniq.io/_mcp/server.

## Opportunity & Project Phase Tracking

### Improved: Phase instance timestamps on `GET /opportunities/{opportunity_id}`

Each entry in `phaseInstances` now includes `startedAt` and `completedAt`, matching the projects endpoint. Use these to compute time-in-stage or close dates without a separate lookup.

* Only populated on `GET /opportunities/{opportunity_id}`; the list and search endpoints do not return `phaseInstances` and are unaffected.
* `completedAt` is `null` for the phase the opportunity currently occupies and any that have not been started.

### New: Filter `GET /opportunities`/`GET /projects` (and their `/search` variants) by phase entry/exit date

Find records by when they entered or exited a phase — e.g. "opportunities that entered Closed - Won this month" — in a single query, without paging through the whole list.

| Param                              | Description                                                                                                                                                            |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `entered_phase_id`                 | Phase template ID to filter by entry date. Required when `entered_after`/`entered_before` is given; given alone, matches records that ever entered the phase.          |
| `entered_after` / `entered_before` | ISO 8601 date or date-time bounds on when the record entered `entered_phase_id` (`before` is exclusive).                                                               |
| `exited_phase_id`                  | Phase template ID to filter by exit date. Required when `exited_after`/`exited_before` is given; given alone, matches records that have exited the phase at any point. |
| `exited_after` / `exited_before`   | ISO 8601 date or date-time bounds on when the record exited `exited_phase_id` (`before` is exclusive).                                                                 |

* Look up a phase's template ID via the `phaseTemplateId` on any `phaseInstances` entry from `GET /opportunities/{opportunity_id}`, `GET /projects/{project_id}`, or `GET /workflows`.
* A `*_phase_id` is required whenever its matching date bound(s) are supplied; omitting it returns a `400 VALIDATION_ERROR`. Invalid date formats also return `400`.
* When `entered_phase_id`/`exited_phase_id` is used, each row in the response also includes `phaseTimeline` — the record's full phase-transition history in compact form (`phaseTemplateId`, `startedAt`, `completedAt`), so you can see when it entered/exited without a second call per record. Not present on unfiltered list/search calls, and not the same as `phaseInstances` (no names/positions/SLA — use the get-by-id endpoint for that).

### Fixed: `phaseInstances` on `GET /opportunities/{opportunity_id}` was unordered and missing SLA fields

`phaseInstances` now returns in position order and includes `phaseTemplate.redSla`/`yellowSla`, matching the projects endpoint and the documented schema.

### Fixed: `primaryPhone` list filter required an exact match

`GET /opportunities`/`GET /projects` (and their `/search` variants) `primaryPhone` filter did an exact match against the raw stored phone value, so any human-entered format (dashes, spaces, parens) returned nothing. Now normalizes to digits and substring-matches against both the record's own phone and its linked contact's phone.