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

# Application APIs (Private Preview)

> Drive the application experience from your own backend, including card design selection

<Note>
  This page is intentionally **not listed in the documentation navigation**. The
  Application APIs are in private preview and this page is shared by direct link
  only. Endpoints described here that are not yet generally available are likewise
  excluded from the [REST API reference](/api-reference).
</Note>

## Overview

The Application APIs let you own the apply experience in your own product rather
than launching the hosted flow described in
[Apply for a loan or credit card](/creating-an-application).

Most partners should use the SDK. This surface exists for partners who have agreed
with Imprint to render the application themselves, and it carries additional
compliance obligations — you own the disclosure presentation and must pass a
compliance review before production.

<Warning>
  **Status:** private preview. Endpoint availability is enabled per product. Contact
  your Imprint team before building against this page — a key that works for other
  endpoints will return `403` here until `APPLICATION_WRITE` is allowlisted for
  your product and a key explicitly granting it is provisioned. Ordinary
  [rotation](/api-key-rotation) copies a key's existing scopes; it does not add
  newly allowlisted scopes.
</Warning>

All twelve endpoints require the `x-imprint-merchant-key` header. **That header is
the only thing that names your program** — there is no `product_id` in any request
body. The key must be authorized for the named program, and a product-bound key
does not make the header optional. All twelve operations, including reads, require
`APPLICATION_WRITE`; `APPLICATION_READ` alone does not grant access to this preview.

## The two things to understand first

Everything else on this page follows from these two ideas.

### Configuration drives form construction; the application drives form progression

There are two sources of truth and they answer different questions.

`GET /v2/application_configuration` says what your program *can ever* ask for — the
field set, their validation rules, the disclosures, whether phone verification is
Imprint's or yours. Cache it by `configuration_revision`, and fetch the revision
pinned on an application when resuming that application. Build your form components
from it.

The `requirements` array on an application says what is outstanding for **this**
applicant **right now**. Read it after every write to decide what to render next.

Two guarantees hold between them:

* An application never names a `key` that is absent from the configuration.
* The configuration never asserts what is required for a *given* application. A
  `CONDITIONALLY_REQUIRED` field may or may not be asked for.

FIELD keys are declared in `fields`, DISCLOSURE keys in `disclosures`, and ACTION
or SELECTION keys in `requirement_definitions`. Those three collections together
contain every key the application can return.

### Requirements are a list, not a set of fields

Fields, disclosures, selections, and actions all appear as entries in one array,
each with a `kind`, a `status`, and the milestone it `blocks`:

```json theme={null}
{
  "key": "applicant.ssn",
  "kind": "FIELD",
  "status": "MISSING",
  "blocks": "SUBMIT"
}
```

| `kind`       | What it is                               | How it is satisfied                          |
| ------------ | ---------------------------------------- | -------------------------------------------- |
| `FIELD`      | Applicant data                           | `PATCH /v2/applications/{id}`                |
| `DISCLOSURE` | Consent to be recorded                   | `PATCH` with the disclosure id in `consents` |
| `SELECTION`  | A choice among options, e.g. card design | `PATCH`                                      |
| `ACTION`     | Something with a side effect             | Its own `POST` sub-resource                  |

| `status`    | Meaning                                                                                         |
| ----------- | ----------------------------------------------------------------------------------------------- |
| `MISSING`   | Not yet provided                                                                                |
| `PENDING`   | Started, awaiting completion elsewhere — an OTP sent but unverified, an IDV session in progress |
| `SATISFIED` | Done                                                                                            |
| `INVALID`   | Provided but rejected; `message` says why, and is safe to show the applicant                    |

Because everything is a list entry, **a new requirement is an added array element,
not a new response field.** Handle unknown `kind` and `key` values by ignoring
them. A client that switches exhaustively on `kind` and throws on the default will
break the first time a program adds a requirement; one that skips what it does not
recognize keeps working.

<Warning>
  **`blocks` names the earliest milestone gated, not the only one.** The milestones
  nest: a `PHONE_VERIFICATION` requirement blocks submission too, because nothing
  can be submitted before the phone is verified.

  To find everything holding up submit, take every unsatisfied requirement whose
  `blocks` is `PHONE_VERIFICATION` **or** `SUBMIT`. Counting only `blocks: SUBMIT`
  gives the wrong number, and `SUBMIT_BLOCKED` will disagree with you.

  `OFFER_ACCEPTANCE` and `COMPLETION` requirements do not block submission —
  `authentication` can be satisfied after an offer is accepted.
</Warning>

## The endpoints

|    | Endpoint                                              | Scope               | Idempotent |
| -- | ----------------------------------------------------- | ------------------- | ---------- |
| 1  | `GET /v2/application_configuration`                   | `APPLICATION_WRITE` | —          |
| 2  | `GET /v2/applications`                                | `APPLICATION_WRITE` | —          |
| 3  | `POST /v2/applications`                               | `APPLICATION_WRITE` | yes        |
| 4  | `GET /v2/applications/{id}`                           | `APPLICATION_WRITE` | —          |
| 5  | `PATCH /v2/applications/{id}`                         | `APPLICATION_WRITE` | —          |
| 6  | `POST /v2/applications/{id}/phone_verification`       | `APPLICATION_WRITE` | yes        |
| 7  | `POST /v2/applications/{id}/prefill`                  | `APPLICATION_WRITE` | —          |
| 8  | `POST /v2/applications/{id}/verify_account_ownership` | `APPLICATION_WRITE` | —          |
| 9  | `POST /v2/applications/{id}/authentication`           | `APPLICATION_WRITE` | —          |
| 10 | `POST /v2/applications/{id}/submit`                   | `APPLICATION_WRITE` | yes        |
| 11 | `POST /v2/applications/{id}/idv`                      | `APPLICATION_WRITE` | —          |
| 12 | `POST /v2/applications/{id}/accept`                   | `APPLICATION_WRITE` | yes        |

The four marked idempotent accept an `Idempotency-Key` header retained for 24
hours. Replaying a key returns the original semantic result instead of doing the
thing twice. A request still running returns `409
IDEMPOTENCY_IN_PROGRESS` with `Retry-After`; reusing the key for a different
operation, target, action, or body returns `409 IDEMPOTENCY_KEY_REUSED`.

A typical successful flow calls 1, 3, 6, 7, 5, 9, 10, 4 (polling), then 12.

***

## 1. Retrieve the application configuration

```
GET /v2/application_configuration
```

What your program can ask for. Omit `revision` for current configuration. When
resuming an application, pass its `configuration_revision` as `revision` so field,
requirement, and disclosure definitions match the pinned application.

<CodeGroup>
  ```json Response (200) theme={null}
  {
    "configuration_revision": "ACFG-2026-08-02-1",
    "fields": [
      {
        "key": "applicant.first_name",
        "type": "string",
        "necessity": "ALWAYS_REQUIRED",
        "min_length": 1,
        "max_length": 40
      },
      {
        "key": "applicant.email",
        "type": "email",
        "necessity": "ALWAYS_REQUIRED",
        "max_length": 254
      },
      {
        "key": "applicant.ssn",
        "type": "string",
        "necessity": "ALWAYS_REQUIRED",
        "pattern": "^\\d{9}$",
        "min_length": 9,
        "max_length": 9
      },
      {
        "key": "applicant.income_type",
        "type": "enum",
        "necessity": "OPTIONAL",
        "allowed_values": ["SALARY", "HOURLY", "SELF_EMPLOYED", "OTHER"]
      }
    ],
    "disclosures": [
      {
        "id": "DISC-sms-v1",
        "version": "1",
        "url": "https://disclosures.imprint.co/sms-v1.pdf",
        "gates": "PHONE_VERIFICATION"
      },
      {
        "id": "DISC-ssa-v1",
        "version": "1",
        "url": "https://disclosures.imprint.co/ssa-v1.pdf",
        "body": [
          "Authorization for the Social Security Administration to Disclose Your Social Security Number Verification",
          "I agree to authorize the Social Security Administration (SSA) to verify and disclose to Imprint Payments, for the purpose of this credit card application, whether the name, Social Security Number (SSN) and date of birth I have submitted matches the information in SSA records. My consent is for a one-time validation within the next 30 days."
        ],
        "acknowledgement": "Check this box to provide your electronic signature for the authorization above.",
        "gates": "SUBMIT"
      }
    ],
    "requirement_definitions": [
      {
        "key": "phone_verification",
        "kind": "ACTION",
        "blocks": "PHONE_VERIFICATION"
      },
      {
        "key": "card_design",
        "kind": "SELECTION",
        "blocks": "OFFER_ACCEPTANCE"
      },
      {
        "key": "account_ownership",
        "kind": "ACTION",
        "blocks": "SUBMIT",
        "resolution_options": [
          { "type": "VERIFY_PASSWORD" },
          { "type": "HOSTED_AUTHENTICATION" },
          { "type": "SET_PASSWORD" }
        ]
      }
    ],
    "legal_documents": [
      {
        "id": "DOC-cardholder-agreement-v1",
        "display_name": "Cardholder Agreement",
        "url": "https://disclosures.imprint.co/cardholder-agreement-v1.pdf"
      }
    ],
    "phone_verification": {
      "mode": "IMPRINT_OTP",
      "code_length": 6,
      "resend_after_ms": 15000
    },
    "authentication": {
      "supported_types": ["PASSWORD"],
      "password_policy": {
        "min_length": 8,
        "max_length": 128,
        "pattern": "^(?:[\\s\\S]*\\p{L}[\\s\\S]*\\p{Nd}|[\\s\\S]*\\p{Nd}[\\s\\S]*\\p{L})[\\s\\S]*$",
        "description": {
          "en-US": "At least 8 characters, including a letter and a number.",
          "es-US": "Al menos 8 caracteres, incluyendo una letra y un número."
        }
      }
    },
    "card_design": {
      "default_card_design_id": null
    }
  }
  ```
</CodeGroup>

Cache configurations by `configuration_revision`, not only by product. Ordinary
configuration and disclosures remain pinned for an in-flight application. If a
legal revision is revoked, the server assigns the replacement revision while
preserving candidate and consent history, and returns the replacement disclosure as
unsatisfied. Refetch the application's new `configuration_revision` and present it
before retrying. Product shutdowns still take effect immediately.

### Fields

`type` chooses the input, `pattern` / `min_length` / `max_length` / `allowed_values`
give you client-side validation identical to what the server enforces, and
`necessity` is one of `ALWAYS_REQUIRED`, `CONDITIONALLY_REQUIRED`, or `OPTIONAL`.

**No label is published, deliberately.** What a field is *called* belongs to the
surface it appears on — it changes with that surface and needs translating alongside
the rest of your page. Validation is published because the program enforces it
server-side, and a client inventing its own would reject input the program accepts.

`necessity: CONDITIONALLY_REQUIRED` means "sometimes." Do not try to work out when.
The application's `requirements` array tells you, per application.

### Disclosures

`url` always points at the document. When wording is legally significant the
program also publishes `body` — an array of paragraphs you must render **verbatim**,
in order — and `acknowledgement`, the text that goes next to the consent control. An
e-signed authorization is the case that forces this: a link alone is not enough for
text that must appear on the page.

`gates` says which milestone the consent unlocks. An SMS disclosure gates
`PHONE_VERIFICATION` and must be consented before a code can be sent.

`legal_documents` is a separate, flat list for a "documents" or "terms" section. It
is not consent — nothing in it needs acknowledging.

### Password policy

Publish-what-you-enforce: `min_length`, `max_length`, a `pattern`, and a per-locale
`description` you can show under the input.

<Warning>
  **The `pattern` must compile in your engine, and the two obvious spellings do not
  port.** RE2 (Go) has no lookahead, so `(?=.*\p{L})(?=.*\p{Nd})` will not compile
  there; Go's `(?s)` is a syntax error in JavaScript. What the program publishes is
  an alternation over orderings across `[\s\S]`, which compiles in both.

  **In JavaScript, `new RegExp(pattern, 'u')` — the `u` flag is required, not
  cosmetic.** Without it a browser reads `\p{L}` as a literal `p` and *silently*
  enforces a rule the program never published.

  The classes are Unicode rather than `[A-Za-z0-9]` because programs publish
  Spanish, and an ASCII class rejects `contraseña1` for containing no letters.

  **If a pattern will not compile, fall open — do not fall closed.** The server
  enforces the rule regardless, and a submit button that never enables is worse than
  a rejection that explains itself.
</Warning>

### Applicant-facing copy is a locale dictionary

Every `message` a shopper reads — `description` above, and the `message` on
requirements, resolution options, and outcome reasons — is a dictionary keyed by
IETF locale tag rather than a single string:

```json theme={null}
{
  "en-US": "Set a password for your new Imprint account",
  "es-US": "Cree una contraseña para su nueva cuenta Imprint"
}
```

You know which locale to render and the server does not — a shopper who switches
language mid-application would otherwise need every requirement refetched. Fall back
to `en-US`, then to any locale present. **A missing translation must degrade to
readable copy in the wrong language, never to a blank** where the applicant most
needs to be told something.

Error bodies are plain English strings. They are developer-facing, and a client
rendering a raw API error to a shopper has a bug in any language.

***

## 2. List applications

```
GET /v2/applications?partner_customer_id=…&status=…&limit=…&starting_after=…
```

Most recently created first. This is how you **resume an abandoned application**:
before creating a new one, look for an existing `IN_PROGRESS` application for that
`partner_customer_id`.

<CodeGroup>
  ```json Response (200) theme={null}
  {
    "data": [
      {
        "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
        "configuration_revision": "ACFG-2026-08-02-1",
        "status": "IN_PROGRESS",
        "partner_customer_id": "PARTNER_USER_456",
        "requirements": [
          {
            "key": "phone_verification",
            "kind": "ACTION",
            "status": "MISSING",
            "blocks": "PHONE_VERIFICATION"
          },
          {
            "key": "applicant.ssn",
            "kind": "FIELD",
            "status": "MISSING",
            "blocks": "SUBMIT"
          }
        ],
        "applicant": { "phone": "+14155550123" },
        "created_at": "2026-08-02T12:00:00Z",
        "updated_at": "2026-08-02T12:00:00Z",
        "expires_at": "2026-08-09T12:00:00Z"
      }
    ],
    "has_more": false,
    "total": 1
  }
  ```
</CodeGroup>

***

## 3. Create an application

```
POST /v2/applications
Idempotency-Key: <your key>
```

Every applicant field is optional. Pass whatever you already hold and fill in the
rest with `PATCH`. There is no `product_id` — `x-imprint-merchant-key` already names
the program, and a body field naming it again would be a second source of truth with
no stated precedence.

<CodeGroup>
  ```json Request theme={null}
  {
    "partner_customer_id": "PARTNER_USER_456",
    "applicant": {
      "phone": "+14155550123"
    }
  }
  ```

  ```json Response (201) theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "IN_PROGRESS",
    "partner_customer_id": "PARTNER_USER_456",
    "requirements": [
      {
        "key": "DISC-sms-v1",
        "kind": "DISCLOSURE",
        "status": "MISSING",
        "blocks": "PHONE_VERIFICATION",
        "disclosure": {
          "id": "DISC-sms-v1",
          "version": "1",
          "url": "https://disclosures.imprint.co/sms-v1.pdf",
          "gates": "PHONE_VERIFICATION"
        }
      },
      {
        "key": "phone_verification",
        "kind": "ACTION",
        "status": "MISSING",
        "blocks": "PHONE_VERIFICATION"
      },
      {
        "key": "applicant.first_name",
        "kind": "FIELD",
        "status": "MISSING",
        "blocks": "SUBMIT"
      },
      {
        "key": "applicant.ssn",
        "kind": "FIELD",
        "status": "MISSING",
        "blocks": "SUBMIT"
      },
      {
        "key": "DISC-esign-v1",
        "kind": "DISCLOSURE",
        "status": "MISSING",
        "blocks": "SUBMIT",
        "disclosure": {
          "id": "DISC-esign-v1",
          "version": "1",
          "url": "https://disclosures.imprint.co/esign-v1.pdf",
          "body": [
            "Cardholder Agreement, which describes interest rate and fee information, Privacy Notice, Consent to Receiving Electronic Communications, Credit Report Authorization, How We May Contact You, Rewards Program Terms & Conditions."
          ],
          "acknowledgement": "Check this box to acknowledge that you have received, read, and agreed to all the terms above.",
          "gates": "SUBMIT"
        }
      },
      {
        "key": "card_design",
        "kind": "SELECTION",
        "status": "MISSING",
        "blocks": "OFFER_ACCEPTANCE"
      },
      {
        "key": "authentication",
        "kind": "ACTION",
        "status": "MISSING",
        "blocks": "COMPLETION",
        "resolution_options": [
          {
            "type": "SET_PASSWORD",
            "message": {
              "en-US": "Set a password for your new Imprint account",
              "es-US": "Cree una contraseña para su nueva cuenta Imprint"
            }
          }
        ]
      }
    ],
    "applicant": { "phone": "+14155550123" },
    "created_at": "2026-08-06T12:00:00Z",
    "updated_at": "2026-08-06T12:00:00Z",
    "expires_at": "2026-08-13T12:00:00Z"
  }
  ```
</CodeGroup>

A newly created application always has an unsatisfied `phone_verification`
requirement. The response is the whole application, so you have your first render
without a follow-up `GET`.

***

## 4. Retrieve an application

```
GET /v2/applications/{application_id}
```

**This is the only endpoint you need to poll.** After submit, wait `poll_after_ms`
milliseconds between calls rather than fixing your own interval. It is `null` when no
evaluation is in flight — polling again will not change the result.

<CodeGroup>
  ```json Response (200) — mid-decision theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "PENDING_DECISION",
    "poll_after_ms": 1500,
    "requirements": [
      { "key": "applicant.ssn", "kind": "FIELD", "status": "SATISFIED" }
    ],
    "outcome": null,
    "created_at": "2026-08-06T12:00:00Z",
    "updated_at": "2026-08-06T12:00:03Z",
    "expires_at": null
  }
  ```

  ```json Response (200) — decided theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "OFFERS_AVAILABLE",
    "poll_after_ms": null,
    "offers": [
      {
        "id": "OFFR-6DB3F865E2EACCF0A77D4330",
        "credit_limit": 530000,
        "apr_percentage": 24.99,
        "expires_at": "2026-08-20T12:00:04Z"
      }
    ],
    "outcome": { "result": "APPROVED" },
    "created_at": "2026-08-06T12:00:00Z",
    "updated_at": "2026-08-06T12:00:04Z",
    "expires_at": null
  }
  ```
</CodeGroup>

`credit_limit` is in minor units, so `530000` is \$5,300.00.

<Note>
  **`expires_at` is `null` once the application is submitted.** It governs
  abandonment of an unsubmitted application, and there is nothing left to abandon. Do
  not read it as a deadline for accepting an offer — the offer carries its own
  `expires_at`.
</Note>

***

## 5. Update an application

```
PATCH /v2/applications/{application_id}
```

Applicant data, disclosure consent, and card design selection. **JSON merge-patch
semantics:** omitted fields are untouched, an explicit `null` clears a field. A
multi-page form can send only the current page's delta without wiping earlier pages.
The rule is recursive: `applicant.address.city: null` clears only the city, while
`applicant.address: null` clears the address. `metadata` merges by key, a null
metadata value removes that key, and `metadata: null` clears the map. Zero remains a
value rather than being treated as missing. `consents` is additive: omission or `[]`
adds nothing, while `consents: null` is rejected and never withdraws a receipt.

<CodeGroup>
  ```json Request — fields and consents together theme={null}
  {
    "applicant": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "ada@example.com",
      "address": {
        "street_line1": "1 Main St",
        "city": "Austin",
        "state": "TX",
        "postal_code": "78701"
      },
      "date_of_birth": { "year": 1990, "month": 4, "day": 12 },
      "ssn": "123456789",
      "annual_income": 9000000
    },
    "consents": ["DISC-sms-v1", "DISC-ssa-v1", "DISC-esign-v1"]
  }
  ```

  ```json Request — one consent on its own theme={null}
  {
    "consents": ["DISC-sms-v1"]
  }
  ```

  ```json Request — card design selection theme={null}
  {
    "card_design_id": "3b9c1f3e-52a0-44c1-b131-a7ab0099a214"
  }
  ```

  ```json Request — nested clears and zero values theme={null}
  {
    "applicant": {
      "address": { "street_line2": null },
      "housing_cost": 0
    },
    "metadata": {
      "campaign": null,
      "referrer": "fall-launch"
    }
  }
  ```

  ```json Response (200) — one field rejected theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "IN_PROGRESS",
    "requirements": [
      {
        "key": "applicant.first_name",
        "kind": "FIELD",
        "status": "SATISFIED"
      },
      {
        "key": "applicant.address",
        "kind": "FIELD",
        "status": "INVALID",
        "blocks": "SUBMIT",
        "message": {
          "en-US": "Address could not be verified",
          "es-US": "No se pudo verificar la dirección"
        }
      }
    ]
  }
  ```
</CodeGroup>

The full application comes back, so **the refreshed `requirements` array arrives with
the write.** You never need to follow a `PATCH` with a `GET`.

`annual_income` is in minor units. `consents` names disclosure ids from the
configuration; recording one flips that `DISCLOSURE` requirement to `SATISFIED`. A
field the program rejects is safely retained as the editable candidate and comes
back as `INVALID` with a localized `message`, rather than failing the whole request.
Clear or replace it in a later patch. Malformed JSON, unknown or disallowed fields,
and wrong JSON types return `400` without mutating the application.

**This endpoint carries everything the applicant typed and could retype.** Anything
with an external side effect or a one-way transition — sending an SMS, verifying a
code, writing a credential, committing a decision — is a separate `POST`
sub-resource. That is the line the other seven writes sit on the far side of.

Applicant and identity data freeze at submit. Card design remains editable through
`OFFERS_AVAILABLE`, or can be supplied to accept, and freezes when acceptance
commits so issuance cannot race a later selection. A `409` means the requested field
is not editable in the application's current phase, or the draft has expired.

***

## 6. Verify the applicant's phone number

```
POST /v2/applications/{application_id}/phone_verification
Idempotency-Key: <your key>
```

One endpoint, three actions. Which ones you may use is program configuration
(`phone_verification.mode`), not a property of the route — so the contract is
identical across partners.

| `action` | What happens                                                       | Available when                            |
| -------- | ------------------------------------------------------------------ | ----------------------------------------- |
| `SEND`   | Imprint sends an SMS code                                          | `mode: IMPRINT_OTP`                       |
| `VERIFY` | You submit the code the applicant received                         | `mode: IMPRINT_OTP`                       |
| `ATTEST` | You certify that *you* verified the number, skipping Imprint's SMS | `mode: PARTNER_ATTESTED` only, else `403` |

<CodeGroup>
  ```json Request — SEND theme={null}
  {
    "action": "SEND"
  }
  ```

  ```json Response (200) — after SEND theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "IN_PROGRESS",
    "requirements": [
      {
        "key": "DISC-sms-v1",
        "kind": "DISCLOSURE",
        "status": "SATISFIED",
        "disclosure": {
          "id": "DISC-sms-v1",
          "version": "1",
          "url": "https://disclosures.imprint.co/sms-v1.pdf",
          "gates": "PHONE_VERIFICATION"
        }
      },
      {
        "key": "phone_verification",
        "kind": "ACTION",
        "status": "PENDING",
        "blocks": "PHONE_VERIFICATION"
      }
    ]
  }
  ```

  ```json Request — VERIFY theme={null}
  {
    "action": "VERIFY",
    "code": "888888"
  }
  ```

  ```json Request — ATTEST theme={null}
  {
    "action": "ATTEST",
    "verified_at": "2026-08-06T12:00:00Z",
    "method": "SMS_OTP"
  }
  ```

  ```json Response (429) — resent too soon theme={null}
  {
    "type": "RESEND_TOO_SOON",
    "message": "a code was sent recently; wait before requesting another",
    "retry_after_seconds": 15
  }
  ```
</CodeGroup>

`SEND` requires the SMS disclosure to be consented first. Until it is, that
disclosure requirement carries `blocks: PHONE_VERIFICATION` and this returns `409`.

Note the `PENDING` in the response above — a code is out but unverified. That is the
state your "enter the code" screen renders from.

Size your code input from `phone_verification.code_length`. A wrong or expired code
is a `422`; too many failed attempts is a `429`.

### Resends are rate-limited

`retry_after_seconds` is duplicated on the `Retry-After` header; it is in the body as
well so a client that already parses the JSON error can render a countdown without
reaching for response headers. **The value rounds up** — truncating would report `0`
with time still on the clock and send you straight into another rejection.

Disable your resend control for `phone_verification.resend_after_ms` so the applicant
is not offered a button that cannot work yet. **But treat the `429` as
authoritative** — it accounts for a code sent before your page loaded, which your
configuration value cannot know about.

Every send is an SMS someone receives and someone pays for. Build the countdown.

### Verifying may surface a new requirement

If the phone already belongs to an Imprint account, the response adds an
`account_ownership` requirement, and its `resolution_options` names the ways forward.
See endpoint 8.

***

## 7. Prefill applicant data

```
POST /v2/applications/{application_id}/prefill
```

Looks the applicant up from their verified phone number plus the last four digits of
their SSN, and applies whatever identity data can be retrieved. Requires a verified
phone number; a `409` means it is not verified yet.

<CodeGroup>
  ```json Request theme={null}
  {
    "ssn_last_four": "8888"
  }
  ```

  ```json Response (200) theme={null}
  {
    "prefilled": {
      "applicant.first_name": "Dana",
      "applicant.last_name": "Whitfield",
      "applicant.address": {
        "street_line1": "1100 Congress Ave",
        "city": "Austin",
        "state": "TX",
        "postal_code": "78701",
        "country": "USA"
      },
      "applicant.date_of_birth": { "year": 1990, "month": 4, "day": 12 }
    },
    "application": {
      "id": "APLN-v1-698F7593-B009-4AC6-AF02-37B3F865E2EA",
      "configuration_revision": "ACFG-2026-08-02-1",
      "status": "IN_PROGRESS",
      "requirements": [
        { "key": "applicant.first_name", "kind": "FIELD", "status": "SATISFIED" },
        { "key": "applicant.address", "kind": "FIELD", "status": "SATISFIED" },
        {
          "key": "applicant.email",
          "kind": "FIELD",
          "status": "MISSING",
          "blocks": "SUBMIT"
        },
        {
          "key": "applicant.ssn",
          "kind": "FIELD",
          "status": "MISSING",
          "blocks": "SUBMIT"
        }
      ]
    }
  }
  ```

  ```json Response (404) — no match theme={null}
  {
    "type": "NO_PREFILL_MATCH",
    "message": "no applicant record matched"
  }
  ```
</CodeGroup>

The values are applied to the application **and** returned to you, keyed by field
`key`, so you can render a confirm-and-edit screen. **Present them for confirmation
rather than submitting silently** — the applicant must be able to correct a wrong
line.

### A miss is a 404, and you must branch on `type`

`NO_PREFILL_MATCH` is an ordinary outcome, not a fault: send the applicant to manual
entry. The application is left untouched.

**Branch on the error `type`, not the status alone.** `NO_PREFILL_MATCH` is the
applicant's path forward; `APPLICATION_NOT_FOUND` on the same status code means the
id was wrong and is *your* bug — it must not be shown to the applicant as though
their data went missing.

<Warning>
  **A prefill hit does not satisfy `applicant.ssn`.** Notice in the response above
  that `applicant.ssn` is still `MISSING` — submit requires all nine digits.

  `ssn_last_four` is an input to this endpoint only and is deliberately not a field
  in `configuration.fields`. Four digits are guessable, and waiving the full number
  on that evidence would turn a convenience into an identity oracle.
</Warning>

***

## 8. Verify ownership of an existing Imprint account

```
POST /v2/applications/{application_id}/verify_account_ownership
```

Satisfies the `account_ownership` requirement when the applicant's phone number
already belongs to an Imprint account. Follow the advertised resolution option:
use `PASSWORD` for `VERIFY_PASSWORD`, or `HOSTED` for
`HOSTED_AUTHENTICATION` (including PIN-only accounts).

Not to be confused with `phone_verification`, which verifies the *number*. This
proves the applicant is the person who owns the account that number is attached to.

<CodeGroup>
  ```json Request theme={null}
  {
    "type": "PASSWORD",
    "password": "correct horse battery staple"
  }
  ```

  ```json Response (200) theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "IN_PROGRESS",
    "requirements": [
      { "key": "account_ownership", "kind": "ACTION", "status": "SATISFIED" }
    ]
  }
  ```

  ```json Request — initiate hosted ownership theme={null}
  {
    "type": "HOSTED"
  }
  ```

  ```json Response (200) — hosted ownership initiated theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "IN_PROGRESS",
    "requirements": [
      { "key": "account_ownership", "kind": "ACTION", "status": "PENDING" }
    ],
    "ownership_session": {
      "url": "https://authenticate.imprint.co/s/eyJhbGciOiJIUzI1NiJ9",
      "expires_at": "2026-08-06T12:10:03Z"
    }
  }
  ```

  ```json Response (409) — nothing to satisfy theme={null}
  {
    "type": "NO_OWNERSHIP_REQUIREMENT",
    "message": "this application has no account_ownership requirement to satisfy"
  }
  ```
</CodeGroup>

<Note>
  **`type` is `PASSWORD`, not `VERIFY_PASSWORD`.** The `VERIFY_PASSWORD` you saw in
  `resolution_options` names which *endpoint* to call; it is not a value to pass
  through as a field. See [Resolution options](#resolution-options).
</Note>

`ownership_session` is short-lived and appears only in the response that initiates
the hosted handoff. Do not persist the URL; ordinary GETs omit it. Completing the
handoff updates the authoritative application, which you retrieve by polling.

When the existing account has neither a password nor a PIN, the requirement's
`resolution_options` says `SET_PASSWORD`. Use endpoint 9 only after verified phone
has produced the Imprint-issued setup grant; phone knowledge alone never grants
ownership of a pre-existing account.

A wrong credential is a `422`. **The message is identical whether or not an account
exists on that phone number** — this endpoint is reachable with only a phone number,
so a distinguishable failure would make it an account-existence oracle. Do not try to
infer account existence from it.

Too many attempts returns the same `429` shape as endpoint 6, with
`retry_after_seconds`.

***

## 9. Establish authentication

```
POST /v2/applications/{application_id}/authentication
```

Sets how the customer will authenticate. This is a credential write, so it is
deliberately separate from `PATCH` — it gets its own audit trail and is never
replayable as form data.

| `type`            | Meaning                                                                                                                                                                        |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PASSWORD`        | The customer sets an Imprint password. Application-API integrations set a password rather than a PIN, because a password-backed account stays recoverable without the partner. |
| `PARTNER_MANAGED` | You own authentication end to end and the customer has no Imprint login. Available only to programs with a full Servicing-API integration.                                     |

<CodeGroup>
  ```json Request theme={null}
  {
    "type": "PASSWORD",
    "password": "correct horse battery 9"
  }
  ```

  ```json Response (200) theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "IN_PROGRESS",
    "requirements": [
      { "key": "authentication", "kind": "ACTION", "status": "SATISFIED" }
    ]
  }
  ```

  ```json Response (400) — too short theme={null}
  {
    "type": "INVALID_REQUEST",
    "message": "password must be at least 8 characters"
  }
  ```

  ```json Response (400) — fails the pattern theme={null}
  {
    "type": "INVALID_REQUEST",
    "message": "password must include at least one letter and one number"
  }
  ```
</CodeGroup>

Validate against the published `password_policy` before calling, so the applicant
gets an inline hint instead of a round-trip. The server enforces the same rule.
`password` is write-only and is never returned by any endpoint.

The `authentication` requirement `blocks: COMPLETION`, not `SUBMIT` — so you may
satisfy it any time before the application completes, including after an offer is
accepted. Doing it *before* accept means the accept call lands on `COMPLETED`
directly rather than parking in `PENDING_AUTHENTICATION`.

***

## 10. Submit for decisioning

```
POST /v2/applications/{application_id}/submit
Idempotency-Key: <your key>
```

No request body. Everything blocking submission must be satisfied first.

<CodeGroup>
  ```json Response (200) theme={null}
  {
    "id": "APLN-v1-650E5076-F94C-4E6C-9583-9DED76DB3F86",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "PENDING_DECISION",
    "poll_after_ms": 1500,
    "requirements": [
      { "key": "phone_verification", "kind": "ACTION", "status": "SATISFIED" },
      { "key": "applicant.ssn", "kind": "FIELD", "status": "SATISFIED" }
    ],
    "outcome": null,
    "expires_at": null
  }
  ```

  ```json Response (409) — blocked theme={null}
  {
    "type": "SUBMIT_BLOCKED",
    "message": "2 requirements must be satisfied before this application can be submitted",
    "blocking_requirements": [
      {
        "key": "applicant.ssn",
        "kind": "FIELD",
        "status": "MISSING",
        "blocks": "SUBMIT"
      },
      {
        "key": "DISC-esign-v1",
        "kind": "DISCLOSURE",
        "status": "MISSING",
        "blocks": "SUBMIT"
      }
    ]
  }
  ```
</CodeGroup>

Decisioning is asynchronous. Poll endpoint 4 honouring `poll_after_ms`, or wait for
the [application webhook](/application-event-notifications) — the `status` vocabulary
is identical between the two, so a polled value and a webhook value can be compared
directly.

`blocking_requirements` is the exact subset holding things up, in the same shape as
`requirements` — so you can route the applicant straight back to the right screen
without diffing anything. Remember that this includes `PHONE_VERIFICATION`
requirements, per the nesting rule above.

### The outcomes

Six shapes come out of decisioning. A client that handles only approval handles a
fraction of the contract.

<CodeGroup>
  ```json OFFERS_AVAILABLE theme={null}
  {
    "status": "OFFERS_AVAILABLE",
    "offers": [
      {
        "id": "OFFR-650E5076F94CE6C95839DED7",
        "credit_limit": 530000,
        "apr_percentage": 24.99,
        "expires_at": "2026-08-16T12:00:03Z"
      }
    ],
    "outcome": { "result": "APPROVED" }
  }
  ```

  ```json REJECTED theme={null}
  {
    "status": "REJECTED",
    "outcome": {
      "result": "REJECTED",
      "reason_code": "INSUFFICIENT_CREDIT_HISTORY",
      "reasons": [
        {
          "code": "INSUFFICIENT_CREDIT_HISTORY",
          "message": {
            "en-US": "Not enough credit history",
            "es-US": "Historial de crédito insuficiente"
          }
        }
      ],
      "adverse_action_notice_url": "https://disclosures.imprint.co/adverse-action-notice/APLN-v1-650E5076-F94C-4E6C-9583-9DED76DB3F86/notice.pdf"
    }
  }
  ```

  ```json NO_OFFERS_AVAILABLE theme={null}
  {
    "status": "NO_OFFERS_AVAILABLE",
    "outcome": {
      "result": "REJECTED",
      "reason_code": "NO_PRODUCT_MATCH",
      "reasons": [
        {
          "code": "NO_PRODUCT_MATCH",
          "message": {
            "en-US": "No product matched this applicant",
            "es-US": "Ningún producto coincidió con este solicitante"
          }
        }
      ],
      "adverse_action_notice_url": "https://disclosures.imprint.co/adverse-action-notice/APLN-v1-650E5076-F94C-4E6C-9583-9DED76DB3F86/notice.pdf"
    }
  }
  ```

  ```json IDV_REQUIRED theme={null}
  {
    "status": "IDV_REQUIRED",
    "outcome": {
      "result": "ACTION_REQUIRED",
      "recoverable": true,
      "reason_code": "IDENTITY_VERIFICATION_REQUIRED",
      "reasons": [
        {
          "code": "IDENTITY_VERIFICATION_REQUIRED",
          "message": {
            "en-US": "Additional verification needed",
            "es-US": "Se necesita verificación adicional"
          }
        }
      ],
      "recovery_options": [
        {
          "type": "IDV",
          "message": {
            "en-US": "Verify identity with a government ID",
            "es-US": "Verifique su identidad con una identificación oficial"
          }
        }
      ]
    }
  }
  ```

  ```json PENDING_REVIEW theme={null}
  {
    "status": "PENDING_REVIEW",
    "poll_after_ms": 1500,
    "outcome": {
      "result": "PENDING_REVIEW",
      "reason_code": "MANUAL_REVIEW",
      "reasons": [
        {
          "code": "MANUAL_REVIEW",
          "message": {
            "en-US": "A person will look at this application",
            "es-US": "Una persona revisará su solicitud"
          }
        }
      ]
    }
  }
  ```
</CodeGroup>

<Warning>
  **`NO_OFFERS_AVAILABLE` and `REJECTED` share `outcome.result: REJECTED`**, and both
  carry an adverse action notice. They are separate statuses because they are
  different things to say to an applicant: `REJECTED` is a decline on this
  application, while `NO_OFFERS_AVAILABLE` means underwriting completed and produced
  nothing the program could offer.

  **Branch on `status`, not on `outcome.result`.**
</Warning>

### Reading an outcome

**Read `recoverable` and `recovery_options` to decide what the applicant can do — not
`reason_code`.** A frozen credit file is recoverable by the applicant; an
underwriting decline generally is not. `reason_code` describes *why* and may change
independently of whether anything can be done about it.

`reason_code` and `reasons[].code` only ever grow as vocabularies, and codes are
never repurposed. **Treat an unrecognized code as an unhandled default, not an
error.**

`outcome` is scoped to the most recent decisioning round, not accumulated across all
of them. Resubmitting after recovering replaces the object wholesale — including
`reasons` — so you never have to work out which entries are stale. It is never set
back to `null` once populated: the last completed outcome remains visible while a
recovery decision round runs and is replaced atomically when the new round finishes.
Resubmit is permitted only when that latest outcome is explicitly `recoverable` and
its blockers have cleared. An ordinary repeated submit is a conflict unless it is an
idempotent replay.

Where a recovery step has a corresponding requirement, the same option appears in
both `requirements[].resolution_options` and `outcome.recovery_options`.
**`requirements` is the authority on what to *do*; `recovery_options` is the
summary.** Drive your form off `requirements` and use `outcome` to explain the
decision.

<Warning>
  `adverse_action_notice_url` is present when a notice is required. **It is presigned
  and short-lived** — fetch it or hand it to the applicant when you receive it. Do
  not cache, log, or persist it.
</Warning>

***

## 11. Start identity verification

```
POST /v2/applications/{application_id}/idv
```

For an application in `IDV_REQUIRED`. Returns a hosted URL to send the applicant to.
`redirect_url` must be allowlisted for your program; it is optional, and omitting it
lands the applicant on an Imprint completion page.

<CodeGroup>
  ```json Request theme={null}
  {
    "redirect_url": "https://partner.example.com/apply/idv-complete"
  }
  ```

  ```json Response (200) theme={null}
  {
    "url": "https://verify.imprint.co/s/eyJhbGciOiJIUzI1NiJ9",
    "expires_at": "2026-08-06T12:10:03Z"
  }
  ```

  ```json Response (409) — already done theme={null}
  {
    "type": "IDV_ALREADY_COMPLETED",
    "message": "identity verification is already complete"
  }
  ```

  ```json Response (409) — never needed theme={null}
  {
    "type": "IDV_NOT_REQUIRED",
    "message": "this application does not require identity verification"
  }
  ```
</CodeGroup>

The URL is **single-use and short-lived**. Request a fresh one rather than caching
it.

When the applicant finishes, the application returns to `PENDING_DECISION` and
decisioning resumes. **A successful IDV is not itself an approval** — poll or wait
for the webhook as you would after submitting. That is a second wait your client has
to handle.

The two `409`s are separated because the right response differs.
`IDV_ALREADY_COMPLETED` is a race the applicant already won — stop asking and poll
for the decision. `IDV_NOT_REQUIRED` means you reached this endpoint without an
`IDV_REQUIRED` status, which is a dead end and a bug in your flow.

***

## 12. Accept an offer

```
POST /v2/applications/{application_id}/accept
Idempotency-Key: <your key>
```

Accepts one of the offers from `OFFERS_AVAILABLE` and creates the Imprint account.
You may pass `card_design_id` at the same time, which is equivalent to selecting it
via `PATCH` beforehand. Acceptance commits the effective selected/default design;
that value is then immutable and is the design used for card issuance.

<CodeGroup>
  ```json Request theme={null}
  {
    "offer_id": "OFFR-6DB3F865E2EACCF0A77D4330"
  }
  ```

  ```json Request — with design selection theme={null}
  {
    "offer_id": "OFFR-6DB3F865E2EACCF0A77D4330",
    "card_design_id": "3b9c1f3e-52a0-44c1-b131-a7ab0099a214"
  }
  ```

  ```json Response (200) theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "OFFER_ACCEPTED",
    "customer_id": null,
    "card_design_id": "3b9c1f3e-52a0-44c1-b131-a7ab0099a214",
    "outcome": { "result": "APPROVED" }
  }
  ```

  ```json Response (200) — provisioning complete theme={null}
  {
    "id": "APLN-v1-6DB3F865-E2EA-4CF0-A77D-4330C5CE3A15",
    "configuration_revision": "ACFG-2026-08-02-1",
    "status": "COMPLETED",
    "partner_customer_id": "PARTNER_USER_456",
    "customer_id": "2EE24580-B97B-4949-A65C-929CCB9B9B8D",
    "requirements": [
      { "key": "card_design", "kind": "SELECTION", "status": "SATISFIED" },
      { "key": "authentication", "kind": "ACTION", "status": "SATISFIED" }
    ],
    "applicant": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "ada@example.com",
      "phone": "+14155550123",
      "address": {
        "street_line1": "1 Main St",
        "city": "Austin",
        "state": "TX",
        "postal_code": "78701"
      },
      "date_of_birth": { "year": 1990, "month": 4, "day": 12 },
      "ssn_last_four": "8888",
      "annual_income": 9000000
    },
    "card_design_id": "3b9c1f3e-52a0-44c1-b131-a7ab0099a214",
    "offers": [
      {
        "id": "OFFR-6DB3F865E2EACCF0A77D4330",
        "credit_limit": 530000,
        "apr_percentage": 24.99,
        "expires_at": "2026-08-20T12:00:04Z"
      }
    ],
    "outcome": { "result": "APPROVED" },
    "created_at": "2026-08-02T12:00:00Z",
    "updated_at": "2026-08-06T12:00:04Z"
  }
  ```
</CodeGroup>

Acceptance can outlive the HTTP request. Once the commitment is durable, a response
may truthfully return `OFFER_ACCEPTED` while provisioning continues; `customer_id`
remains null. Poll the application. Replaying the same idempotency key returns the
same semantic acceptance result and never starts another account workflow.

`customer_id` is the existing external Imprint customer identifier used by the
servicing APIs, and is the handle for everything afterwards — the
[servicing reads](/guide-embedded-servicing), transaction intents, rewards. Persist it
against your own customer record.

Note that `ssn` comes back as `ssn_last_four`. Sensitive values are returned masked
or omitted; the application is not a way to read back what was submitted.

**If authentication is not yet established the application waits in
`PENDING_AUTHENTICATION` rather than `COMPLETED`** — call endpoint 9 to finish. The
account exists either way and `customer_id` is populated.

A `409` means there are no offers available, an offer was already accepted, or the
offer expired. A `404` covers an `offer_id` that is not on this application.

***

## Resolution options

`resolution_options` (on a requirement) and `recovery_options` (on an outcome) share
one shape. Their `type` names a **route**, and is a different vocabulary from the
credential `type` those routes accept.

| Option `type`           | Call                                | With body `type` |
| ----------------------- | ----------------------------------- | ---------------- |
| `VERIFY_PASSWORD`       | `POST .../verify_account_ownership` | `PASSWORD`       |
| `HOSTED_AUTHENTICATION` | `POST .../verify_account_ownership` | `HOSTED`         |
| `SET_PASSWORD`          | `POST .../authentication`           | `PASSWORD`       |
| `IDV`                   | `POST .../idv`                      | —                |
| `UNFREEZE_CREDIT`       | no endpoint                         | —                |

**Do not pass an option `type` through as a request field.** `VERIFY_PASSWORD` in a
body is rejected.

`UNFREEZE_CREDIT` has no endpoint: the applicant lifts the freeze with the bureau
directly, after which the application can be resubmitted.

`resolution_options` is present whenever an action needs to advertise which route
to use, even if only one is currently available. For `account_ownership`,
password-backed accounts verify a password, PIN-only accounts use hosted
authentication, and credential-less accounts set a password through an authorized
setup flow.

## Statuses

```
IN_PROGRESS · PENDING_DECISION · IDV_REQUIRED · PENDING_REVIEW ·
OFFERS_AVAILABLE · NO_OFFERS_AVAILABLE · REJECTED · OFFER_ACCEPTED ·
PENDING_AUTHENTICATION · COMPLETED · EXPIRED
```

| Status                             | Meaning and legal next actions                                                                                                        |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `IN_PROGRESS`                      | Edit draft data, satisfy phone/ownership/authentication actions, then submit. An expired draft moves to `EXPIRED`.                    |
| `PENDING_DECISION`                 | Decisioning is running. Poll; authentication may still be established independently.                                                  |
| `IDV_REQUIRED`                     | Start IDV. Its callback returns the same application to `PENDING_DECISION`; it is not approval.                                       |
| `PENDING_REVIEW`                   | Manual review is outstanding. Poll while backend resolution updates the same application.                                             |
| `OFFERS_AVAILABLE`                 | Choose or change a design and accept one eligible, unexpired offer. A recoverable decision may instead begin another submitted round. |
| `REJECTED` / `NO_OFFERS_AVAILABLE` | Read the outcome and notice. Only an explicitly recoverable outcome may be resubmitted after its blockers clear.                      |
| `OFFER_ACCEPTED`                   | Acceptance is committed and provisioning is running. The design is frozen; poll.                                                      |
| `PENDING_AUTHENTICATION`           | The account exists; satisfy authentication to reach `COMPLETED`.                                                                      |
| `COMPLETED`                        | Account provisioning and required authentication are complete. No application mutation remains.                                       |
| `EXPIRED`                          | The unsubmitted draft expired. Writes are rejected.                                                                                   |

`ACCOUNT_FUNDED` remains part of the shared application-webhook vocabulary for
funding products. This private-preview origination flow does not expose secured
funding or a funding operation, so it does not originate that transition.

Identical to the `status` on the
[application webhook](/application-event-notifications). The vocabulary grows over
time, so handle an unrecognized status as an unhandled default rather than an error.

## Errors

Every error body carries a `type` and a `message`. **`type` is what you branch on** —
`message` is developer-facing English, subject to change, and not for showing to an
applicant.

| Status | Notable `type` values                                                                                                                          |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `INVALID_REQUEST`                                                                                                                              |
| `403`  | Scope not granted, or `ATTEST` on a program not configured for it                                                                              |
| `404`  | `APPLICATION_NOT_FOUND`, `NO_PREFILL_MATCH`                                                                                                    |
| `409`  | `SUBMIT_BLOCKED`, `NO_OWNERSHIP_REQUIREMENT`, `IDV_ALREADY_COMPLETED`, `IDV_NOT_REQUIRED`, `IDEMPOTENCY_IN_PROGRESS`, `IDEMPOTENCY_KEY_REUSED` |
| `422`  | Wrong OTP code, wrong credential                                                                                                               |
| `429`  | `RESEND_TOO_SOON` — with `retry_after_seconds` and a `Retry-After` header                                                                      |

Two statuses carry more than one `type` with genuinely different handling: `404`
(`NO_PREFILL_MATCH` is the applicant's path forward, `APPLICATION_NOT_FOUND` is your
bug) and `409` (the two IDV conflicts above). Branching on status alone will get both
wrong.

For `IDEMPOTENCY_IN_PROGRESS`, wait the rounded-up `retry_after_seconds` (also in
`Retry-After`) and retry the identical request with the same key. For
`IDEMPOTENCY_KEY_REUSED`, do not retry that different request with the same key.

## Card design selection during apply

A customer picking their card is part of the apply flow, not an afterthought: in the
distinct-cards model the physical card is only issued once a design is selected (see
[Card issuance](/guide-unified-card-experience)). If you render the application, you
render the design picker, which means you need the design catalog.

Two APIs cover this, and both are **generally available today** — unlike the rest of
this page, they are published in the [REST API reference](/api-reference) and need no
private-preview enablement.

### Retrieving the list of all card designs

```
GET /v2/card_designs
```

Returns the card designs available for your program. If your program defines no
designs of its own, the designs configured for your partner are returned instead.

| Parameter        | In    | Description                                                                                                                                                                                |
| ---------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `customer_id`    | query | Restrict results to designs this customer is eligible for, based on their loyalty tier. Omit it — on a pre-signup teaser screen, for example — to receive every design the program offers. |
| `search_term`    | query | Filters designs by name.                                                                                                                                                                   |
| `starting_after` | query | Pagination cursor. Pass the previous response's `next_page_token`.                                                                                                                         |

<CodeGroup>
  ```json Response (200) theme={null}
  {
    "data": [
      {
        "id": "3b9c1f3e-52a0-44c1-b131-a7ab0099a214",
        "name": "Midnight Black",
        "access_min_rank": 0,
        "type": "REGULAR",
        "status": "ACTIVE",
        "image_orientation": "VERTICAL",
        "light_asset_url_path": "https://assets.imprint.co/brand-assets/card-designs/light/midnight-black.svg",
        "dark_asset_url_path": "https://assets.imprint.co/brand-assets/card-designs/dark/midnight-black.svg"
      }
    ],
    "has_more": false,
    "total": 1
  }
  ```
</CodeGroup>

**`customer_id` controls whether tier-gated designs appear at all.** Omit it and you
receive every design the program offers, each carrying its `access_min_rank` — the
minimum loyalty tier rank needed to select it, where `0` means available to everyone.
Supply it and designs above that customer's tier are **filtered out of the response
entirely**, so every design you receive is one they can select.

This is the choice that determines what your picker can show:

| Call                                 | Returns                               | Use it for                                                             |
| ------------------------------------ | ------------------------------------- | ---------------------------------------------------------------------- |
| `GET /v2/card_designs`               | Every design, gated ones included     | A locked state — show the design greyed out with its `access_min_rank` |
| `GET /v2/card_designs?customer_id=…` | Only designs this customer can select | A picker where every option is selectable                              |

If you want to render locked designs, call it **without** `customer_id` and compare
`access_min_rank` against the customer's tier yourself. Passing `customer_id` and
expecting to find locked designs in the response will not work — they are absent, not
flagged.

Only `ACTIVE` designs can be selected.

<Note>
  `loyalty_tier_name` is currently always omitted, so you cannot label a locked design
  with the tier name it requires. Use `access_min_rank` and map it to your own tier
  copy.
</Note>

<Note>
  `starting_after` on this endpoint takes the opaque `next_page_token` from the
  previous response, **not** a card design id as other list endpoints do. Treat the
  token as opaque — its contents may change.
</Note>

Pass the selected design's `id` as `card_design_id` on `PATCH /v2/applications/{id}`
or on accept. See
[The card the customer is actually holding](/guide-embedded-servicing#the-card-the-customer-is-actually-holding)
for reading back the design that was actually printed on the issued card.

### Rendering the artwork

`light_asset_url_path` and `dark_asset_url_path` are **absolute URLs**, served from
`https://assets.imprint.co` or `https://app.imprint.co` depending on where the
artwork was uploaded. Render them directly; do not prepend a host of your own.

Use `image_orientation` (`VERTICAL` or `HORIZONTAL`) to lay the artwork out — it
describes the artwork itself, so a horizontal asset rendered in a vertical frame will
look wrong. `categories` carries partner-defined key/value labels if you want to group
designs into sections in your picker.

## Sequencing

The apply flow crosses the boundary described above: before the customer exists there
is no `customer_id` to pass, and after they exist there is. Those two calls return
**different result sets**, so treat them as two separate fetches:

1. **Pre-signup teaser** — call `GET /v2/card_designs` with no `customer_id`. Every
   design, gated ones included.
2. **The real picker** — once the customer exists, call it again **with**
   `customer_id`. Now scoped to what they can actually select.

Cache each response for as long as you are on that side of the boundary — designs
change on the order of program configuration changes, not per request. **Do not carry
the teaser response forward into the picker.** It contains designs the customer may be
ineligible for, and confirming with one of those ids will fail. Refetch instead of
reusing the cache once `customer_id` is available.

## A working sequence, end to end

```
GET  /v2/application_configuration                  → cache for the deploy
GET  /v2/applications?partner_customer_id=…         → resume, or:
POST /v2/applications                               → APP id, requirements

PATCH .../{id}          consents: [DISC-sms-v1]     → SMS disclosure SATISFIED
POST  .../phone_verification   {action: SEND}       → phone_verification PENDING
POST  .../phone_verification   {action: VERIFY}     → SATISFIED
                                                      (may add account_ownership)

POST  .../prefill       {ssn_last_four}             → 200 hit, or 404 NO_PREFILL_MATCH
PATCH .../{id}          applicant + consents        → fields SATISFIED
PATCH .../{id}          card_design_id              → card_design SATISFIED
POST  .../authentication {type, password}           → authentication SATISFIED

POST  .../submit                                    → PENDING_DECISION
GET   .../{id}          honour poll_after_ms        → OFFERS_AVAILABLE
                                                      (or IDV_REQUIRED → POST .../idv,
                                                       then poll again)

POST  .../accept        {offer_id}                  → COMPLETED, customer_id
```

## Checklist before you go to production

The traps that cost the most time, in the order you will hit them:

* [ ] Unknown `kind`, `key`, `status`, and `reason_code` values are **ignored, not thrown on**.
* [ ] `SUBMIT_BLOCKED` counts include `blocks: PHONE_VERIFICATION`, not just `blocks: SUBMIT`.
* [ ] The published `password_policy.pattern` compiles in your engine — with the `u` flag in JavaScript — and **falls open** if it does not.
* [ ] `message` fields are read as locale dictionaries with an `en-US` fallback, not printed as objects.
* [ ] A prefill `404` branches on `type`: `NO_PREFILL_MATCH` advances the flow, `APPLICATION_NOT_FOUND` is an error you log.
* [ ] A prefill hit still collects the full nine-digit SSN.
* [ ] Resend is disabled for `resend_after_ms`, and re-armed from the `429`'s `retry_after_seconds`.
* [ ] Polling honours `poll_after_ms` rather than a fixed interval.
* [ ] `NO_OFFERS_AVAILABLE`, `REJECTED`, `PENDING_REVIEW`, and `IDV_REQUIRED` each render something sensible — approval is one of six outcomes.
* [ ] The IDV round-trip handles the *second* wait: finishing IDV returns to `PENDING_DECISION`, not to an offer.
* [ ] `adverse_action_notice_url` is never cached, logged, or persisted.
* [ ] `Idempotency-Key` is set on create, phone verification, submit, and accept.
