---
title: "Preflight Enrich Row Async"
method: POST
path: "/api/v1/enrichment/enrich_async/{enrichment_id}/preflight"
tags: ["enrichment"]
---

# Preflight Enrich Row Async

`POST /api/v1/enrichment/enrich_async/{enrichment_id}/preflight`

## Request

- Base URL: `https://api.maia-analytics.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.maia-analytics.com/api/v1/enrichment/enrich_async/{enrichment_id}/preflight`
- Auth: HTTP bearer

## Request body

- EnrichRowRequest: Request schema for enriching rows. Two mutually-exclusive selection modes: - ``feature_ids`` — explicit ids the client has materialized (legacy path; row-detail trigger and small-N enrich-from-cell). - ``selection`` — the filter spec the client saw at confirm time; the server recomputes the matching id set and fingerprint/count under a live session. Used by the column-wide enrich confirm gate where the id list would exceed wire-payload limits. Exactly one must be provided; the model-validator enforces this so a request that drops both or duplicates both is rejected with 422.
  - `feature_ids` string[], nullable
  - `selection` FilterSelectionRequest: Snapshot-consumer payload. Caller asserts: "when I called ``/filtered-count`` with this spec I saw ``expected_count`` rows and the server returned this ``fingerprint``; act on that selection." The server recomputes both and rejects with ``SelectionDriftResponse`` on mismatch.
    - `filter_spec` SSRTableQueryParams, required: Combined query-param shape for the 5 AG-Grid SSR endpoints. Aliases match AG Grid's camelCase wire format (``startRow``, ``filterModel``, …) so the client contract is unchanged. Handlers parse JSON-encoded query strings into this model via the ``parse_ssr_query_params`` dep; any validation failure surfaces as 422 with a field path.
      - `startRow` integer
      - `endRow` integer
      - `sortModel` SortEntry[], nullable
        - `colId` string, required
        - `sort` 'asc' | 'desc', required
        - `sortMode` 'default' | 'null_push' | 'count', nullable
      - `filterModel` object, nullable
      - `filterId` string, uuid, nullable
      - `groupKeys` union[], nullable
        - union
          - string
          - integer
          - number
          - boolean
      - `rowGroupCols` RowGroupCol[], nullable
        - `id` string, required
        - `field` string, nullable
        - `displayName` string, nullable
      - `searchText` string, nullable
      - `geometry` GeoJSONPolygon: Narrow GeoJSON subset — matches the spatial-filter payload the frontend draws. Accept Polygon and MultiPolygon only; reject Point / LineString / feature collections, which ``ST_Intersects`` with a bbox wouldn't produce meaningful results against.
        - `type` 'Polygon' | 'MultiPolygon', required
        - `coordinates` unknown[], required
          - unknown
      - `favoritesOnly` boolean
    - `enrichment_state` string, nullable
    - `enrichment_name` string, nullable
    - `fingerprint` string, required
    - `expected_count` integer, required
  - `selection_limit` integer, nullable
  - `allowance_confirmation_fingerprint` string, nullable
  - `high_priority` boolean: Execute with high priority if True
  - `point_action` boolean: True for a deliberate single-cell enrichment. Only a point action skips the coordinator; column kickoffs stay coordinator-keyed even when they resolve to a single row.

## Response `200`

Successful Response

- EnrichmentAdmissionPreflightResponse
  - `admitted` boolean, required
  - `decision` DiscoveryAllowanceAdmissionDetail, required
    - `code` 'allowed' | 'budget_exhausted' | 'budget_unavailable' | 'run_unaffordable' | 'confirmation_required' | 'paid_access_blocked', required: Every verdict admission can reach, for one budget. The vocabulary was two vocabularies: one spelled for the free allowance and one for the paid balance, saying the same four things twice. They are one now because there is one balance to exhaust and one check to be refused by. What a workspace paid for that balance is not in here — it decides what a refusal offers next, which the client reads off the workspace. ``PAID_ACCESS_BLOCKED`` is the exception and stays: it reports a subscription that stopped paying, and a balance nobody was charged for cannot reach that state.
    - `budget_authority` 'credits' | 'paid_balance', required: Which budget a metered run draws down. Two members, because there are two budgets. Free usage and paid usage are the same balance on the same account, spent at the same prices and refused by the same check; what a workspace paid for that balance decides what its refusal offers next, not how the run is admitted. A third member for the free tier is what let the two drift — different prices, different exemptions, different per-run sizing — while both claimed to meter usage.
    - `message` string, required
    - `requested_rows` integer, nullable
    - `affordable_rows` integer, nullable
    - `selection_fingerprint` string, nullable
    - `billing_status` 'pending' | 'active' | 'past_due' | 'unpaid' | 'cancelled': Subscription state as MAIA understands it, not as Stripe reports it. ``PAST_DUE`` is the retry window: full access with a visible warning. ``UNPAID`` is that window exhausted — paid work refuses until payment succeeds, but the subscription has not ended, so purchased usage is kept. ``CANCELLED`` is paid access ended. Purchased usage is kept there too — no status takes it — but only a status that keeps paid access may spend it.

## Other responses

- `422`: Validation Error

## Changes

- **2026-09-12** `604ea84d0ff7`: 6 warning, 6 info
  - deleted the `query` request parameter `token`
  - added the new `budget_exhausted` enum value to the `decision/code` response property for the response status `200`
  - added the new `budget_unavailable` enum value to the `decision/code` response property for the response status `200`
  - added the new `confirmation_required` enum value to the `decision/code` response property for the response status `200`
  - …8 more
- **2026-08-27** `e17aaa23de12`: 1 warning
  - added the new `discovery_allowance_run_unaffordable` enum value to the `decision/code` response property for the response status `200`
- **2026-08-22** `136e4af4d514`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/maia-analytics/apis/maia-api/changes/api/v1/enrichment/enrich_async/:enrichment_id/preflight/post.md)

---

[API](https://skmtc.dev/maia-analytics/apis/maia-api.md) - [All operations](https://skmtc.dev/maia-analytics/apis/maia-api/llms.txt) - [OpenAPI document](https://skmtc.dev/maia-analytics/apis/maia-api/revisions/bb508e935174?raw)
