Credit Card APR Refresh

This guide walks through how to request a refreshed Annual Percentage Rate (APR) for a credit card and read the updated value back from Spinwheel.

Overview

Credit Card APR Refresh lets you request an up-to-date APR for a user's credit card. At a high level, you will:

  1. Retrieve the user and check whether the credit card supports APR refresh.
  2. Start a liability refresh request with the ANNUAL_PERCENTAGE_RATE capability.
  3. Wait for a terminal refresh status by webhook, or poll the refresh status endpoint.
  4. Retrieve the user again and read the refreshed APR from data.creditCards[].aprs.

APR refresh is an on-demand, asynchronous request.

How This Fits With Other Refresh Flows

If you have already integrated another Spinwheel refresh flow, APR refresh should feel familiar:

  • Like Real-Time Balance, you check a liability capability, start a refresh, wait for status, and then retrieve the user.
  • You request a specific capability through POST /v1/users/{userId}/liabilities/refresh.
  • APR refresh is a one-time request. It does not create a weekly or monthly subscription.

Before You Start

Use these rules to keep your integration predictable:

  • Check the capabilities matrix before every refresh request.
  • Only request APR for credit cards where annualPercentageRate.availability is SUPPORTED.
  • Use a unique extRequestId for every refresh request.
  • Subscribe to REFRESH_TRANSACTION_STATUS webhooks so you do not have to poll.
  • Call GET /v1/users to read the final APR value, when needed.

Real-time APR retrieval is not available for every credit card or institution. If a card is not eligible, the capabilities matrix will explain why.

Step 1: Check APR Eligibility

Each credit card returned by GET /v1/users includes a capabilities matrix. For APR refresh, check data.creditCards[].capabilities.data.annualPercentageRate.

Supported Example:

{
  "capabilities": {
    "data": {
      "annualPercentageRate": {
        "availability": "SUPPORTED",
        "nextEligibleRefreshOn": 1770000000000
      }
    }
  }
}

Unsupported Example:

{
  "capabilities": {
    "data": {
      "annualPercentageRate": {
        "availability": "NOT_SUPPORTED",
        "description": "INSTITUTION_NOT_SUPPORTED"
      }
    }
  }
}

If availability is not SUPPORTED, do not send a refresh request for that card. The request will not succeed.

When nextEligibleRefreshOn is present, it is the earliest time, in epoch milliseconds, when another APR refresh can be requested for that card.

Step 2: Start the APR Refresh

Call POST /v1/users/:userId/liabilities/refresh and include the credit card ID with the ANNUAL_PERCENTAGE_RATE capability.

{
  "extRequestId": "partner-request-123",
  "creditCards": [
    {
      "id": "007035e0-7865-49f3-be50-c922b1c63dc6",
      "capabilities": ["ANNUAL_PERCENTAGE_RATE"]
    }
  ]
}

The response confirms that the refresh has started. APR refresh rows use capability: "ANNUAL_PERCENTAGE_RATE".

{
  "status": {
    "code": 200,
    "desc": "success"
  },
  "data": [
    {
      "refreshTransactionStatus": "IN_PROGRESS",
      "refreshTransactionId": "b790770b-33c7-49d4-b086-e6e18864ba34",
      "creditCardId": "007035e0-7865-49f3-be50-c922b1c63dc6",
      "capability": "ANNUAL_PERCENTAGE_RATE",
      "nextEligibleRefreshOn": 1770000000000
    }
  ]
}

You can request APR and real-time balance in the same call if the card supports both capabilities. Each card and capability pair is processed as its own refresh transaction.

{
  "extRequestId": "partner-request-124",
  "creditCards": [
    {
      "id": "007035e0-7865-49f3-be50-c922b1c63dc6",
      "capabilities": ["REALTIME_BALANCE","ANNUAL_PERCENTAGE_RATE"]
    }
  ]
}

For multi-card APR requests, Spinwheel validates every requested APR card before creating any APR refresh transaction. If one requested card is unsupported, the request returns a 400 instead of creating a partial set of APR refreshes.

Step 3: Track Refresh Status

APR refresh is asynchronous. The initial API response tells you the refresh is IN_PROGRESS; it does not mean the APR is ready yet.

The recommended approach is to subscribe to the REFRESH_TRANSACTION_STATUS webhook. Spinwheel sends this event when the refresh reaches a terminal state.

You can also poll GET /v1/users/:userId/liabilities/refresh/:extRequestId.

Polling returns refresh transaction status rows. It does not return APR values.

{
  "status": {
    "code": 200,
    "desc": "success"
  },
  "data": [
    {
      "refreshTransactionStatus": "COMPLETED",
      "refreshTransactionId": "b790770b-33c7-49d4-b086-e6e18864ba34",
      "creditCardId": "007035e0-7865-49f3-be50-c922b1c63dc6",
      "capability": "ANNUAL_PERCENTAGE_RATE",
      "nextEligibleRefreshOn": 1770000000000
    }
  ]
}

Step 4: Retrieve the Updated APR

After the refresh status is COMPLETED, call GET /v1/users and read data.creditCards[].aprs.

The refreshed APR includes source: "REAL_TIME" and an updatedOn timestamp.

The same APR shape also appears on credit card entries in the POST /v1/users/:userId/debtProfile response.

Webhook Payloads

APR refresh uses the existing REFRESH_TRANSACTION_STATUS webhook event. A completed APR refresh includes the refreshed APR in data.aprs:

{
  "eventType": "REFRESH_TRANSACTION_STATUS",
  "userId": "user-id",
  "extRequestId": "partner-request-123",
  "refreshTransactionId": "b790770b-33c7-49d4-b086-e6e18864ba34",
  "creditCardId": "007035e0-7865-49f3-be50-c922b1c63dc6",
  "refreshTransactionStatus": "COMPLETED",
  "data": {
    "aprs": [
      {
        "rate": 19.99,
        "type": "PURCHASE",
        "source": "REAL_TIME",
        "updatedOn": 1770000000000
      }
    ]
  }
}

A failed APR refresh uses the same envelope without the data object:

{
  "eventType": "REFRESH_TRANSACTION_STATUS",
  "userId": "user-id",
  "extRequestId": "partner-request-123",
  "refreshTransactionId": "b790770b-33c7-49d4-b086-e6e18864ba34",
  "creditCardId": "007035e0-7865-49f3-be50-c922b1c63dc6",
  "refreshTransactionStatus": "FAILED"
}

Webhook notes:

  • data.aprs is included only when the APR refresh is COMPLETED.
  • If you miss a webhook, poll the refresh status endpoint to recover the terminal status.
  • Polling does not return APR values. After the refresh completes, call GET /v1/users.

Reading APR Values

APR values are returned under data.creditCards[].aprs.

Each APR has a source:

  • DERIVED: inferred from credit report data.
  • REAL_TIME: retrieved directly through an APR refresh.

When a derived APR and a real-time APR exist for the same type, the real-time APR takes precedence. Derived APRs without a real-time replacement are retained, and real-time APRs for new types are appended.

An APR object may include these fields:

{
  "rate": 19.99,
  "type": "PURCHASE",
  "applicableBalance": 2500,
  "expirationDate": "2026-12-31",
  "source": "DERIVED",
  "updatedOn": 1770000000000
}

Implementation Checklist

Before going live, confirm that your integration does the following:

  • Reads annualPercentageRate.availability from the latest GET /v1/users response.
  • Skips cards where APR refresh is not SUPPORTED.
  • Sends ANNUAL_PERCENTAGE_RATE only for eligible credit card IDs.
  • Stores or logs extRequestId and refreshTransactionId for troubleshooting.
  • Handles both COMPLETED and FAILED webhook statuses.
  • Calls GET /v1/users to retrieve the APR, when needed.