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

# Bulk prompt imports

> Submit and monitor atomic, append-only prompt batches through the Public API V2 private preview.

<Warning>
  Public API V2 is a private preview and is disabled by default. The prompt
  import contract is implemented, but the production-readiness gate has not
  passed. Use it only in a preview-enabled workspace and do not treat it as a
  generally available production dependency yet.
</Warning>

Bulk prompt import is an occasional, one-way write workflow. It appends new
prompts to one website and gives you a redacted operation receipt to poll. It
is **not** a prompt-library read, CRUD, replacement, reconciliation, or
synchronization API, and an upload never starts an analysis.

The V2 prompt surface contains exactly two operations:

| Method and route                                                  | Purpose                             |
| ----------------------------------------------------------------- | ----------------------------------- |
| `POST /public/v2/websites/{website_id}/prompts:import`            | Accept one append-only batch.       |
| `GET /public/v2/websites/{website_id}/prompt-imports/{import_id}` | Read that batch's redacted receipt. |

The generated [V2 OpenAPI document](https://api.videntic.com/public/v2/openapi.json)
is the authoritative machine-readable contract.

## Access and authorization

Both operations require a V2-eligible, owner-issued API key with the
`prompts_write` scope. A workspace owner must adopt or rotate an older write key
into a V2 integration lineage before it can call these routes. A read key does
not gain prompt-library access, and `prompts_write` does not add a prompt-list
operation.

```http theme={null}
Authorization: Bearer vid_...
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
```

Missing or invalid authentication returns `401`. An authenticated key that is
not V2-eligible or lacks `prompts_write` returns `403`.

## Submit a batch

Each request must be JSON and must stay within both limits:

* From 1 through 1,000 items.
* At most 5 MiB for the encoded request body.

Send an exact `Content-Type: application/json` media type, optionally with
parameters such as `charset=utf-8`. Invalid JSON returns `400 invalid_request`,
an encoded body over 5 MiB returns `413 payload_too_large`, and a different
media type returns `415 unsupported_media_type`. These failures occur before an
operation is created.

Only `prompt_text` is required on an item. It is trimmed and must contain from
5 through 2,000 Unicode characters. Unknown fields are rejected.

The generated OpenAPI request requires all four top-level fields: `items`,
`defaults`, `classification`, and `validate_only`. Send empty `defaults` as
`{}` when you do not need them, and send the selected classification mode and
validation flag explicitly.

```bash cURL theme={null}
curl --request POST \
  "https://api.videntic.com/public/v2/websites/$WEBSITE_ID/prompts:import" \
  --header "Authorization: Bearer $VIDENTIC_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $IMPORT_KEY" \
  --data '{
    "items": [
      {
        "client_id": "catalog-001",
        "prompt_text": "Which accounting platform is best for a startup?"
      },
      {
        "client_id": "catalog-002",
        "prompt_text": "How does Acme compare with other accounting tools?",
        "category": "Accounting software"
      }
    ],
    "defaults": {
      "language": "en",
      "tags": ["Discovery"],
      "search_intent": "informational"
    },
    "classification": { "mode": "auto" },
    "validate_only": false
  }'
```

`client_id` is an optional caller-owned correlation value. It must be unique
inside the request and match `[A-Za-z0-9._-]{1,128}`. Receipt items preserve
request order and return the zero-based `index` plus `client_id` when supplied.
They never return prompt text.

### Optional fields and defaults

An item can also supply `tags`, `language`, `search_intent`, `category`,
`topic`, and `seed_keyword`. `defaults` can supply only `tags`, `language`, and
`search_intent`.

| Field           | Contract                                                                                                              |
| --------------- | --------------------------------------------------------------------------------------------------------------------- |
| `tags`          | Up to 20 normalized-unique values, each at most 80 characters. Item tags replace rather than merge with default tags. |
| `language`      | Lowercase two-letter ISO 639-1 code, such as `en` or `sv`.                                                            |
| `search_intent` | `informational`, `commercial`, or `transactional`.                                                                    |
| `category`      | String up to 100 characters, or explicit `null`.                                                                      |
| `topic`         | String up to 120 characters, or explicit `null`.                                                                      |
| `seed_keyword`  | String up to 255 characters, or explicit `null`.                                                                      |

## Preserve classification quality

`classification.mode` controls how missing metadata is handled.

### `auto` mode

`auto` is the default classification policy and derives only fields that are
genuinely missing. Send `{ "classification": { "mode": "auto" } }`
explicitly to match the generated OpenAPI contract. The precedence is:

1. An item value, including an explicit `null` on a nullable field.
2. A request default.
3. A value derived by `prompt-classifier-v1`.

The classifier never overwrites caller-provided or defaulted values. It derives
missing language, search intent, journey tags, category, topic, and seed
keyword. Derived search intent uses the three-value enum above. Derived tags
use the standard journey taxonomy:

`Discovery`, `Comparison`, `Evaluation`, `Validation`, `Objections`,
`Alternatives`, and `Implementation`.

Caller-provided custom tags are allowed, but a provided tag array in `auto`
mode must be nonempty and include at least one standard journey tag. Category,
topic, and seed keyword remain subject labels rather than aliases for search
intent.

Every created or would-create item can include field-level classification
provenance. Each field reports a source of `provided`, `defaulted`, `derived`,
`not_applicable`, or `low_confidence`; only `derived` includes a confidence
number. The public classifier version is also returned. Vendor and model names
are not part of the public contract.

Auto mode fails the whole operation if required language, search intent, or a
journey tag cannot be classified confidently. It never silently writes a
partially classified fallback. Optional category, topic, or seed keyword can
instead be recorded as `not_applicable` or `low_confidence`.

### `provided_only` mode

Use `provided_only` when your integration owns its taxonomy or prompt content
must not be sent to the classification provider:

```json theme={null}
{
  "items": [
    {
      "client_id": "catalog-001",
      "prompt_text": "Which accounting platform is best for a startup?",
      "tags": [],
      "language": "en",
      "search_intent": "commercial",
      "category": "Accounting software"
    }
  ],
  "defaults": {},
  "classification": { "mode": "provided_only" },
  "validate_only": false
}
```

This mode never invokes the classifier. Every item needs an effective
`language`, either on the item or in `defaults`. An explicit empty `tags` array
is allowed. Other omitted or nullable classification fields may remain empty.

<Warning>
  `provided_only` opts out of automatic classification completeness. To retain
  the same downstream filtering coverage, supply language, at least one
  standard journey tag, search intent, category, topic, and seed keyword where
  they apply. Omitted fields remain absent rather than being inferred, so
  related classifications and filters can be unavailable. Validate your own
  taxonomy before uploading.
</Warning>

## Validate, then write

Set `validate_only: true` to run normalization, duplicate detection,
classification, and prompt-limit projection without creating prompts. A
successful validation receipt uses `would_create`, `active_count`,
`projected_active_count`, and `prompt_limit`.

Validation is a separate asynchronous operation. It does not reserve prompt
capacity and cannot be promoted into a write, so state may change before the
real upload.

Use two different idempotency keys:

1. Submit `validate_only: true` with a validation key and poll it to a terminal
   receipt.
2. If validation succeeds, submit the same items with `validate_only: false`
   and a new write key.
3. Poll the write receipt independently.

Reusing the validation key for the write returns `409 idempotency_conflict`
because `validate_only` is part of the request fingerprint.

## Idempotency and retry rules

Every POST requires an `Idempotency-Key` containing from 16 through 255 visible
ASCII characters. Generate a random UUID for each new operation and persist it
with the exact request body before sending.

| Submission                                                                                       | HTTP result | Behavior                                                                                                        |
| ------------------------------------------------------------------------------------------------ | ----------: | --------------------------------------------------------------------------------------------------------------- |
| New request                                                                                      |       `202` | Creates one durable operation and returns its receipt and `Location`, even if processing finishes very quickly. |
| Same key, same request after schema defaults and trimming, operation still pending or processing |       `202` | Returns the existing operation and same `Location`; it does not execute again.                                  |
| Same key, same request after schema defaults and trimming, terminal operation                    |       `200` | Returns the existing terminal receipt and same `Location`; it does not execute again.                           |
| Same key, different website, body, item order, or `validate_only` value                          |       `409` | Returns `idempotency_conflict`; no second operation is created.                                                 |

Follow these retry rules:

* If the POST response is lost or transport status is uncertain, resend the
  exact request with the **same** idempotency key.
* JSON object-key order does not affect the fingerprint, but array order does.
  Preserve item and tag order when retrying rather than reconstructing a batch.
* For a pre-acceptance `429`, wait for `Retry-After`, then retry the exact
  request with the same key.
* For a POST `500` or `503`, the acceptance result may be uncertain. Retry the
  exact request with the same key using bounded exponential backoff and jitter;
  idempotency resolves it to at most one operation.
* Replaying a terminal receipt never reruns it. If its error says
  `retryable: true` and `retry_strategy: "new_idempotency_key"`, start a new
  operation with a new key.
* If `retryable` is `false`, do not automate a retry. Correct the request or
  resolve the reported capacity/classification issue first.

## Poll the receipt

The POST response includes a relative `Location` such as:

```http theme={null}
HTTP/1.1 202 Accepted
Location: /public/v2/websites/web_.../prompt-imports/pim_...
Cache-Control: private, no-store
```

Every receipt uses the common V2 success envelope. Every representation
contains `id`, `object`, `website_id`, `status`, `validate_only`, `created_at`,
and `expires_at`:

```json theme={null}
{
  "data": {
    "id": "pim_example",
    "object": "prompt_import",
    "website_id": "web_example",
    "status": "pending",
    "validate_only": false,
    "created_at": "2026-07-17T10:00:00Z",
    "expires_at": "2026-08-16T10:00:00Z"
  },
  "meta": {
    "request_id": "req_example"
  }
}
```

GET that location until `data.status` is `succeeded` or `failed`:

```bash cURL theme={null}
curl "https://api.videntic.com$LOCATION" \
  --header "Authorization: Bearer $VIDENTIC_KEY"
```

Receipt GET always returns `200` while the receipt exists. A `pending` or
`processing` receipt includes `Retry-After: 2`; wait at least that many seconds
before polling again. Terminal receipts omit `Retry-After`.

For a polling GET `429`, preserve the same URL and wait for `Retry-After`. For
GET `500` or `503`, retry the same URL with bounded exponential backoff and
jitter. Polling never creates an operation and does not use an idempotency key.
Stop when the receipt is terminal or its documented `expires_at` has passed.

| Status       | Additional fields                                                                 |
| ------------ | --------------------------------------------------------------------------------- |
| `pending`    | No phase or result fields.                                                        |
| `processing` | `phase` (`validating`, `classifying`, or `committing`) and `started_at`.          |
| `succeeded`  | `summary`, ordered `items`, and `completed_at`.                                   |
| `failed`     | `error`, ordered diagnostic `items`, optional safe `summary`, and `completed_at`. |

Terminal item shapes are closed and safe to decode by `status`:

```json theme={null}
[
  { "index": 0, "client_id": "catalog-001", "status": "created" },
  {
    "index": 1,
    "client_id": "catalog-002",
    "status": "skipped",
    "code": "skipped_active_duplicate"
  }
]
```

A successful validation uses `would_create` instead of `created`. A failed
receipt uses `failed` with `classification_failed` only on classification
failures, `skipped` for already-detected duplicates, and `not_processed` for
other items. Branch on the closed `status` and `code` fields rather than a
message string.

`202` means the operation was durably accepted; it does not mean prompts were
created. Prompts commit only when the terminal receipt is `succeeded` and
`validate_only` is `false`.

## Atomic batches, duplicates, and capacity

Each request is one atomic boundary. A successful write creates every eligible
prompt that is not a duplicate. A classification, prompt-limit, or commit
failure creates none of the submitted prompts; failed item results are
diagnostic and do not mean a partial write occurred.

The API does not silently chunk a request. If your upload exceeds 1,000 items
or 5 MiB, partition it deliberately. Treat each partition as an independent
atomic operation with its own idempotency key, preserve stable `client_id`
values, and record which operations belong to your source upload. Atomicity
does not span partitions.

Prompt deduplication normalizes Unicode, case, and whitespace while preserving
punctuation. The first normalized occurrence in a request wins. Existing
duplicates are skipped rather than updated, reactivated, or reclassified:

| Item code                    | Meaning                                                                                                                         |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `skipped_request_duplicate`  | A later normalized duplicate in this request.                                                                                   |
| `skipped_active_duplicate`   | The canonical prompt already exists and is active.                                                                              |
| `skipped_inactive_duplicate` | The canonical prompt exists but is inactive. Ask a workspace owner to reactivate it in the prompt-management surface if needed. |

Duplicates are safe skips. The summary's three duplicate breakdown counts add
up to `skipped_duplicates`.

The website's `prompt_limit` counts active prompts. A validation reports the
current and projected active counts. If a write would exceed the limit, the
operation fails with `prompt_limit_exceeded`, every prompt that is not a
duplicate remains `not_processed`, and `created` is zero. Remove or deactivate
prompts in the management surface, reduce the batch, then validate again with a
new key.

## Terminal failure strategies

| Operation error                | Retryable | What to do                                                                                                              |
| ------------------------------ | --------: | ----------------------------------------------------------------------------------------------------------------------- |
| `prompt_limit_exceeded`        |        No | Reduce active capacity pressure, then start a new validation/write operation.                                           |
| `classification_failed`        |        No | Supply or correct the failed classification fields, or deliberately choose `provided_only`, then start a new operation. |
| `classification_unavailable`   |       Yes | Retry the complete atomic batch with a new idempotency key.                                                             |
| `commit_conflict`              |       Yes | Retry the complete atomic batch with a new idempotency key.                                                             |
| `processing_deadline_exceeded` |       Yes | Retry the complete atomic batch with a new idempotency key.                                                             |
| `internal_processing_error`    |       Yes | Retry with a new idempotency key only when the receipt marks it retryable.                                              |

A deterministic `classification_failed` receipt identifies failed item indexes
and safe `client_id` values but does not echo content. A transient classifier
dependency is retried internally before it becomes
`classification_unavailable`. Never split and retry only the failed items from
an atomic operation unless you intentionally want a different upload.

## Receipt retention, redaction, and key rotation

Receipts and their idempotency state expire together 30 days after acceptance.
After `expires_at`, GET returns tenant-safe `404 not_found` and the old
idempotency key may be reused. Normalized prompt deduplication still protects
existing prompts.

Receipts contain counts, item indexes, optional `client_id`, closed status/error
codes, timestamps, and field-level classification provenance. They do not
contain prompt text, prompt IDs, tags, languages, intent/category/topic values,
seed keywords, or arbitrary provider details. Submitted content is held only in
encrypted short-lived staging, removed after terminal processing, and subject
to a 24-hour hard TTL.

Receipt lookup is bound to the workspace, website, and integration lineage. If
an owner rotates a key while preserving its lineage, the successor key can
replay the original POST or GET the original receipt during the 30-day window.
An independently issued integration cannot read that receipt and receives the
same `404` as a missing resource. A workspace owner can recover a receipt
through the authenticated management surface.

For an operational rotation, a workspace owner opens [Settings → Public API
Keys](https://app.videntic.com/settings?section=api), selects **Rotate key** on
the existing integration, and replaces the secret in the client's secret
store. Rotation keeps that integration's lineage; creating an unrelated key
does not. If the client no longer has a lineage key, the owner can use **Recover
a prompt import receipt** on the same settings page with the `web_…` and
`pim_…` IDs.

## Migrate the V1 prompt upload

Public API V1 is protocol-deprecated, but its prompt-import route is still
operational today. It does **not** return `410` yet. It will retire without a
redirect only after the V2 production-readiness gate passes and the public
documentation is updated.

Do not migrate by changing only `v1` to `v2`:

| V1 prompt import                                                     | V2 prompt import                                                                     |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `POST /public/v1/prompts/import`                                     | `POST /public/v2/websites/{website_id}/prompts:import`                               |
| `website_id` in the body                                             | Opaque website ID in the route path                                                  |
| `dry_run`                                                            | `validate_only` asynchronous operation                                               |
| `mode` field; V1 accepts `append` and rejects `replace` for API keys | Removed; V2 is always append-only                                                    |
| `partial`                                                            | Removed; each V2 request is atomic                                                   |
| Caller-controlled `source`                                           | Removed; the server records `public_api_v2`                                          |
| Synchronous `200` report                                             | `202` acceptance plus receipt polling; terminal replay is `200`                      |
| Results can echo prompt text                                         | Redacted, ordered results correlated by `index` or `client_id`                       |
| Caller supplies classification fields                                | Missing fields are derived in `auto`, or derivation is disabled with `provided_only` |

### Upgrade checklist

* Request V2 private-preview access for the target workspace.
* Have a workspace owner issue or adopt a V2-eligible `prompts_write` key and
  record its integration lineage in your key-rotation procedure.
* Move website scope from the body to the path and remove `mode`, `partial`,
  and caller-controlled `source`.
* Add stable `client_id` values so redacted receipt items can be reconciled to
  your source file without prompt text.
* Choose `auto` or `provided_only` explicitly and verify taxonomy and
  provenance handling.
* Enforce the 1,000-item and 5 MiB limits client-side. Make any larger
  partitioning explicit; never assume server-side chunking.
* Persist request body, item order, idempotency key, import ID, and receipt
  expiry as one client operation record.
* Implement separate validation and write idempotency keys, `Location`
  handling, `Retry-After` polling, terminal status handling, and the retry
  matrix above.
* Treat a successful validation as a projection, not a reservation. Check the
  terminal receipt for the write before considering the upload complete.
* Test key rotation and same-lineage receipt recovery before cutover.
* Keep the V1 and V2 adapters separate. There is no redirect, dual write, or
  automatic fallback between versions.
* Keep production use disabled until Videntic announces that the prompt-import
  production-readiness gate has passed.

## Private-preview status

This endpoint has not passed its production-readiness gate and has no general
availability or production SLA commitment. Videntic is still validating its
availability, operational monitoring, regression detection, and support
process before a broader release. Preview access and support expectations are
agreed directly with each participating workspace.

Until Videntic announces that the gate has passed, keep production use
disabled, maintain your own client-side operation log, and alert on terminal
failures or operations approaching the 30-minute processing deadline. To
request access or report a preview issue, contact
[api@videntic.com](mailto:api@videntic.com?subject=Public%20API%20V2%20prompt%20import%20preview)
with request IDs, import IDs, timestamps, and affected routes. Never include
API keys or prompt text.

## Related

<CardGroup cols={2}>
  <Card title="Migrate from V1 to V2" icon="arrow-right-arrow-left" href="/api/v2-migration">
    Review the broader version boundary, read migration, and rollback plan.
  </Card>

  <Card title="V2 OpenAPI" icon="code" href="https://api.videntic.com/public/v2/openapi.json">
    Inspect the generated request and receipt schemas.
  </Card>
</CardGroup>
