> ## 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.

# Enriching with Read-Only Servicing

> Bring a customer's balance, credit line, statements, and card art into your own account page

## Overview

Bring relevant account details into your native experience.

A customer who holds your card checks their balance where they already are — in your
app, on your account page — not by leaving for a servicing site. A handful of read-only
endpoints are enough to make that page useful without building a servicing product:

| Read                                        | Endpoint                                                                                                                                                         | Scope          |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| Which account to read                       | [`GET /v2/customers/{customer_id}/accounts`](https://docs.imprint.co/api-reference/customers/list-a-customers-accounts)                                          | `ACCOUNT_READ` |
| Where the customer stands today             | [`GET /v2/customers/{customer_id}/accounts/{account_id}`](https://docs.imprint.co/api-reference/customers/get-customers-account)                                 | `ACCOUNT_READ` |
| What the account earns                      | [`GET /v2/customers/{customer_id}/accounts/{account_id}/rewards_categories`](https://docs.imprint.co/api-reference/customers/list-an-accounts-reward-categories) | `ACCOUNT_READ` |
| What your product earns, without an account | [`GET /v2/rewards/rewards_categories`](https://docs.imprint.co/api-reference/rewards/list-the-products-reward-categories)                                        | None           |
| What their closed cycles looked like        | [`GET /v2/customers/{customer_id}/accounts/{account_id}/statements`](https://docs.imprint.co/api-reference/customers/list-an-accounts-statements)                | `ACCOUNT_READ` |
| Which cards they hold                       | [`GET /v2/customers/{customer_id}/payment_methods`](https://docs.imprint.co/api-reference/customers/list-customers-payment-methods)                              | None           |
| The artwork on one of those cards           | [`GET /v2/payment_methods/{payment_method_id}/card_design`](https://docs.imprint.co/api-reference/payment-methods/retrieve-a-payment-methods-card-design)        | None           |
| The designs they could switch to            | [`GET /v2/card_designs`](https://docs.imprint.co/api-reference/card-designs/list-card-designs)                                                                   | None           |

This guide walks the full sequence and the empty states worth handling before you ship.

## Permissions

All four account servicing reads are gated on one scope your API key does not have by default.

| Scope          | Grants                                                                                                                                                               |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ACCOUNT_READ` | Listing a customer's accounts, reading one account's snapshot and reward categories, and listing that account's closed statements — including the statement PDF link |

<Warning>
  **A key that works for your other calls will return `403` here until two things
  happen:** the scope is allowlisted for your product, and the key is
  [rotated](/api-key-rotation) to pick it up. Allowlisting alone does not change an
  existing key. Ask your Imprint team to enable the scope, then rotate.
</Warning>

The card reads and product-wide reward categories read need no scope of their own, so
you can render card art and product earning details before `ACCOUNT_READ` is enabled for
you — useful for getting those parts of the page in place while the allowlisting is
still in flight.

## Resolving what to read

None of the other three account reads is addressable from a customer id alone: all take
an `account_id` in the path, and the way to learn it is to list the customer's accounts.

```
GET /v2/customers/{customer_id}/accounts
```

<CodeGroup>
  ```json Credit card (200) theme={null}
  {
    "accounts": [
      {
        "account_id": "7C1F9A34-2D6B-4E58-8A03-B15E7D9C4620",
        "account_type": "CREDITCARD"
      }
    ]
  }
  ```

  ```json Secured card (200) theme={null}
  {
    "accounts": [
      {
        "account_id": "D420E6B7-91C5-4F0A-8B72-3E5A6C8D1049",
        "account_type": "SECUREDCREDITCARD"
      }
    ]
  }
  ```
</CodeGroup>

`account_type` is how you tell them apart, and the value matters to your UI: a
`SECUREDCREDITCARD` has a customer-funded security deposit behind its credit line, so
copy like "your credit limit was set by Imprint" is wrong for it. The reads that follow
are identical for both — same fields, same conventions — so only the labelling changes.
`CREDITCARD` and `SECUREDCREDITCARD` are the values a card program returns, and the
field is omitted when the product's type could not be determined.

Three things about this read shape the code around it:

* **It is scoped to your program.** An account the customer holds with another Imprint
  program is never returned, so you do not have to filter the result.
* **`account_type` appears here and not on the account itself.** Carry it across if you
  render it; the per-account read does not repeat it.
* **A customer can hold more than one account.** Most programs issue one, and taking
  the first entry is a reasonable start — but decide deliberately rather than by
  accident, because the day a customer has two, a page that silently shows one of them
  is wrong rather than incomplete.

An `account_id` is stable for the life of the account, so you can cache it against your
own customer record after the first resolution and skip this call on later page loads.
Re-list when the cached id starts answering `404`.

## Where the customer stands today

```
GET /v2/customers/{customer_id}/accounts/{account_id}
```

```json Response (200) theme={null}
{
  "customer_id": "2EE24580-B97B-4949-A65C-929CCB9B9B8D",
  "account_id": "7C1F9A34-2D6B-4E58-8A03-B15E7D9C4620",
  "status": "OPEN",
  "currency": "USD",
  "credit_limit": 500000,
  "available_credit": 344625,
  "current_balance": 155375,
  "overdue_amount": 0,
  "statement_due_date": "2026-07-10T00:00:00Z",
  "apr_percentage": 24.99,
  "as_of": "2026-07-02T14:22:31Z"
}
```

This is the live view: `current_balance` includes activity since the last cycle closed,
and `overdue_amount` moves down as payments land. It is what a balance tile should
render.

`as_of` is when we calculated it — the instant these figures were true. Balances move
between one read and the next, so if you cache or render this alongside your own data,
label it with `as_of` rather than with the time you stored it, and use it to decide which
of two reads is the later one.

**Every amount here is positive and says what the customer owes.** A paid-off or
in-credit account reads as `0`, never a negative number. Amounts are integers in the
smallest unit of `currency` — `155375` is \$1,553.75.

Two absences are normal and worth handling before they show up in support tickets:

* **`statement_due_date` is absent on an account in its first billing cycle.** There is
  no closed statement to owe payment on yet. Render "no payment due yet", not an empty
  date.
* **Any amount can be absent**, and `0` means something different from absent: a
  paid-off balance and an unknown balance must not render identically.

Per-statement figures — the statement balance, the minimum due, interest — are not on
this response. They live on the statements read.

## What the account earns

For an authenticated customer, read the categories attached to the account you already
resolved:

```
GET /v2/customers/{customer_id}/accounts/{account_id}/rewards_categories
```

```json Response (200) theme={null}
{
  "currency": "FanCash",
  "categories": [
    {
      "name": "On-Merchant",
      "description": "Purchases at Fanatics stores",
      "earn_rate": "10.00",
      "type": "ON_MERCHANT",
      "merchants": ["Fanatics", "NFL Shop", "NBA Store"]
    }
  ],
  "special_categories": [
    {
      "name": "Signup Bonus",
      "description": "One-time bonus after first purchase",
      "amount": "$200",
      "type": "SIGNUP"
    }
  ]
}
```

Use the [account reward categories
API](https://docs.imprint.co/api-reference/customers/list-an-accounts-reward-categories)
on a servicing page: it anchors the result to the same customer and account as the
balance and statements. It requires `ACCOUNT_READ` and returns `404` when the customer
or account cannot be resolved.

If you need to show earning categories before you know the customer or account — for
example, on a product overview — use the [product reward categories
API](https://docs.imprint.co/api-reference/rewards/list-the-products-reward-categories)
instead:

```
GET /v2/rewards/rewards_categories
```

The API key determines the product, so this read accepts no product, customer, or
account identifier and needs no additional scope. Both endpoints return the same shape:
`categories` contains standard transaction earning rules and `special_categories`
contains active non-transaction offers such as a signup bonus.

Treat `earn_rate` as a decimal string rather than a floating-point number. It is the
number of reward units earned per one transaction-currency unit, while `currency` names
the reward currency in which those units are denominated. Preserve the string's
configured precision when displaying it.

## The card the customer is actually holding

A balance next to the customer's own card art reads as their account. A balance next to
a generic rectangle reads as a widget. Two calls get you the artwork, and neither needs
a scope.

What you want is the design **actually on their card** — not the program default, and not
whatever they picked during apply, which may not be what was ultimately issued.

Start from the customer's cards, because the design belongs to the card rather than to
the customer or the account:

```
GET /v2/customers/{customer_id}/payment_methods
```

```json Response (200) theme={null}
{
  "data": [
    {
      "id": "DCBFC736-2286-42DD-897D-160DCA80AED2",
      "type": "CARD",
      "customer_id": "2EE24580-B97B-4949-A65C-929CCB9B9B8D",
      "status": "ACTIVE",
      "created_at": "2026-02-13T19:08:07Z",
      "metadata": {},
      "card": {
        "last4": "4318",
        "network": "VISA",
        "card_type": "UNIFIED"
      }
    }
  ]
}
```

Then read the design printed on the card you are rendering:

```
GET /v2/payment_methods/{payment_method_id}/card_design
```

```json Response (200) theme={null}
{
  "selected": true,
  "card_design": {
    "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"
  }
}
```

`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 against the customer's light/dark appearance; do not
prepend a host of your own. Unlike a statement `pdf_url` these are not pre-signed and do
not expire, so they are safe to cache — though a design change swaps them, so prefer a
modest TTL over caching forever. `image_orientation` tells you whether the artwork was
drawn `VERTICAL` or `HORIZONTAL`; read it rather than assuming, or the asset ends up
letterboxed or stretched in a frame built for the other one.

**Read the design per card, not per customer.** A customer holding several cards can have
a different design on each, so a page rendering one card should read that card's design.
Pair it with `card.last4` from the list response so the customer can tell which of their
cards the balance belongs to.

<Note>
  **`selected: false` is a normal state, not an error.** Some programs assign a default
  design asynchronously, so a freshly issued card can legitimately have no design
  resolved yet: the response is `200` with `selected: false` and no `card_design`. A loan
  or bank account payment method also returns `200` with `selected: false` — only `CARD`
  payment methods carry a design.

  Render your own placeholder in that case and re-read later. `404` means something
  different: this program has no such payment method.
</Note>

Two notes on the list call. It returns every payment method type, so filter on
`type == "CARD"` before looking for a design. And it takes an optional
`PCI_DETAILS_READ` scope that adds full PAN, CVV, and expiry to the response — you do
not need it here, and a card-art-and-balance page is better off without it.

### Showing the rest of the design catalog

The read above answers "what is on this card". To show the alternatives alongside it — a
design picker in your servicing UI — you also need the program's catalog:

```
GET /v2/card_designs
```

Returns every card design available for your program rather than one card's design. Also
scope-free.

| Parameter        | In    | Description                                                                                                                                     |
| ---------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `customer_id`    | query | Restrict results to designs this customer is eligible for, based on their loyalty tier. Designs above their tier are omitted from the response. |
| `search_term`    | query | Filters designs by name.                                                                                                                        |
| `starting_after` | query | Pagination cursor. Pass the previous response's `next_page_token`, **not** a card design id.                                                    |

**Choose based on whether you want to show locked designs.** `customer_id` is a hard
filter, not a hint: designs above the customer's loyalty tier are omitted from the
response rather than flagged.

| Call                                 | Returns                               | Use it for                                                 |
| ------------------------------------ | ------------------------------------- | ---------------------------------------------------------- |
| `GET /v2/card_designs?customer_id=…` | Only designs this customer can select | A picker where every option is selectable                  |
| `GET /v2/card_designs`               | Every design, gated ones included     | A picker that shows locked designs as an upgrade incentive |

For a servicing picker, `customer_id` is usually what you want — a customer offered a
design they cannot select is a support ticket. If you deliberately want to show locked
designs, omit `customer_id` and compare each design's `access_min_rank` against the
customer's tier yourself; `0` means available to everyone. Only `ACTIVE` designs can be
selected. If your program defines no designs of its own, the designs configured for your
partner are returned instead.

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

Results are paginated — when `has_more` is `true`, pass `next_page_token` back as
`starting_after` for the next page.

### Rendering both together

A picker needs both calls: `GET /v2/card_designs` for the options and
`GET /v2/payment_methods/{payment_method_id}/card_design` for the one currently on the
card, matched on `id` to mark the current selection. The catalog alone cannot tell you
which design the customer has, and the per-card read alone cannot tell you what else
they could choose.

One edge case to handle: the card's current design **may not appear in the catalog
response**. A design can be set to `INACTIVE` after it was printed, and a tier-gated
design stays on the card if the customer's tier later drops — in both cases the per-card
read returns it while a `customer_id`-scoped catalog omits it. Render the current design
from the per-card response rather than by looking its `id` up in the catalog, or it will
silently disappear from your UI.

See
[Application APIs](/partner-application-apis#retrieving-the-list-of-all-card-designs)
for the same catalog read used during apply, where it is fetched without a `customer_id`
for pre-signup screens — and where the design is *selected*, by passing `card_design_id`
on the application. There is no partner endpoint that changes the design on an
already-issued card.

## Statement history

```
GET /v2/customers/{customer_id}/accounts/{account_id}/statements?limit=12
```

```json Response (200) theme={null}
{
  "data": [
    {
      "statement_id": "8A31B2C0-4F6D-4A21-9E3B-5C1D0F7A2B84",
      "customer_id": "2EE24580-B97B-4949-A65C-929CCB9B9B8D",
      "account_id": "7C1F9A34-2D6B-4E58-8A03-B15E7D9C4620",
      "period_start_date": "2026-05-16T00:00:00Z",
      "period_end_date": "2026-06-15T00:00:00Z",
      "due_date": "2026-07-10T00:00:00Z",
      "pdf_url": "https://…/2026-06-15.pdf?…",
      "currency": "USD",
      "statement_balance": 155375,
      "minimum_due": 3500,
      "previous_balance": 120000,
      "purchases": 45375,
      "payments": -10000,
      "interest": 0,
      "as_of": "2026-07-02T14:22:31Z"
    }
  ],
  "has_more": false
}
```

Statements are newest first, and a cycle appears here **once it has closed** — the
cycle in progress is not a statement. A year of history (`limit=12`) is usually the
right depth for an embedded list; `limit` accepts 1–100 and defaults to 10. When
`has_more` is `true`, pass `next_page_token` back as `starting_after`. That token is
opaque and is **not** a statement id; a value this endpoint did not issue answers `400`
rather than serving page one.

`as_of` here dates the **read**, not the cycle — `period_end_date` is when the cycle
closed. Every row on a page carries the same value, and `pdf_url` expires an hour after
it, so it is also the clock your link-refresh logic should work from.

### Amounts are signed here, unlike on the account

These amounts follow the convention on a paper statement: what the customer was charged
is positive, what was credited back to them is negative. So `purchases`, `interest`, and
`fees` are positive while `payments`, `credits`, and `refunds` are negative. Nothing is
clamped, because here the sign is the information — flattening it makes a payment
indistinguishable from a month with no activity.

Do not reuse your account-page formatter on these fields. The two responses deliberately
disagree: `/accounts/{account_id}` answers "what do you owe", and a statement answers
"what happened in this cycle".

As on the account, only the lines a cycle had activity on are reported. A quiet month
omits `refunds` rather than sending `0`.

### The statement PDF link is short-lived

`pdf_url` is a pre-signed link to the same document the cardholder sees. **It expires
one hour after the response that carried it**, so treat it as a click target, not
something to store: a URL persisted in your database will stop working, and re-reading
the statement is how you get a fresh one.

Two practical consequences:

* **Keep the link fresher than its lifetime if the list can sit open.** Re-reading the
  page every 15 minutes or so is enough; refreshing on click is not, because awaiting a
  request takes the navigation out of the user's gesture and browsers block the popup.
* **Redact it from logs.** A pre-signed URL is a bearer credential for that document
  for the next hour. Logging the response verbatim publishes the statement to anyone
  who can read your logs.

`pdf_url` is also **absent while the document is still rendering**. A statement's PDF
is produced shortly after its cycle closes, so the newest entry can arrive before its
document does; the rest of the statement is accurate. Render the row without a download
link rather than hiding the row.

## Handling the empty states

A servicing tile is one section of a page you own, so nothing here should be able to
fail that page. Every way of having nothing to show has a defined answer, and it is
worth mapping each to its own copy:

| Condition                                | What you see                                           | Render                                                  |
| ---------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- |
| Customer holds no card with your program | Customer search returns an empty list                  | Your pre-application state                              |
| Customer exists, no account yet          | `404` from list accounts, or an empty `accounts` array | "Your application is still being reviewed"              |
| Account open, first cycle not closed     | `200` with an empty `data` array                       | "Your first statement will be available after DD Month" |
| Newest statement not yet rendered        | Statement present, `pdf_url` absent                    | The row, without a download link                        |
| Card design not assigned yet             | `200` with `selected: false`                           | Your own card placeholder                               |
| Scope missing or Imprint unreachable     | `403`, `5xx`, or a timeout                             | "Account details are temporarily unavailable"           |

Two of these are easy to conflate and shouldn't be. A `404` means the account genuinely
is not there; anything else means the read failed. And an empty `data` array from a
readable account is not a failure at all — it is a real customer whose first cycle has
not closed.

The pattern that keeps this manageable: resolve the customer once, then let each section
return "unavailable, because X" instead of an error. The account tile, the statement
list, reward categories, and the card art share that resolution but fail independently,
so one read timing out costs you that section and not the balance or the artwork.

## A working sequence, end to end

```
1. GET /v2/customers?partner_customer_id=…            → customer_id
2. GET /v2/customers/{customer_id}/accounts           → account_id, account_type
3. GET /v2/customers/{customer_id}/accounts/{account_id}
                                                      → balance, credit line, due date
4. GET /v2/customers/{customer_id}/accounts/{account_id}/rewards_categories
                                                      → earning categories + offers
5. GET /v2/customers/{customer_id}/accounts/{account_id}/statements?limit=12
                                                      → closed cycles + PDF links
6. GET /v2/customers/{customer_id}/payment_methods    → payment_method_id, last4
7. GET /v2/payment_methods/{payment_method_id}/card_design
                                                      → light/dark artwork URLs
```

Steps 3–5 are independent once step 2 resolves, so issue them concurrently. Step 6
does not depend on step 2 at all — the card art hangs off the customer, not the account —
so it can run alongside step 2. Step 1 is skippable if you already hold the Imprint
`customer_id` from [account linking](/account-linking) or a webhook. For a page that
only needs product-wide earning categories, call `GET /v2/rewards/rewards_categories`
without running this account-resolution sequence.

## Checklist before you go to production

* [ ] `ACCOUNT_READ` allowlisted for your product, and the API key
  [rotated](/api-key-rotation) afterwards
* [ ] Account reward categories used for servicing, or product reward categories used
  when no customer or account is available
* [ ] Reward `earn_rate` values kept as decimal strings with their configured precision
* [ ] Account amounts formatted as "what you owe" (always positive), statement amounts
  formatted with their sign preserved
* [ ] Absent amounts distinguished from `0` in your rendering
* [ ] `statement_due_date` absent handled as "no payment due yet"
* [ ] `pdf_url` re-read rather than stored, and redacted from logs
* [ ] A statement row renders without `pdf_url`
* [ ] Card art read per payment method, filtered to `type == "CARD"`, with a placeholder
  for `selected: false` and `image_orientation` honoured
* [ ] If you render a design catalog, the current design comes from the per-card read
  rather than from a lookup in the catalog response
* [ ] Each empty state in the table above has its own copy, and none of them fails the
  surrounding page
* [ ] Pagination passes `next_page_token` back verbatim as `starting_after`
