full-swing / personal-loans
stable v1.0 internal

Personal Loans API

A single integration surface for any NerdWallet team member to build, test, and measure a landing page hypothesis against the Personal Loans marketplace — with zero instrumentation overhead. Session creation, input capture, marketplace rendering, and revenue analytics are exposed as four discrete endpoints.

01
POST /session
get a sub_id
02
POST /inputs
capture form fields
03
GET /marketplace
render offers
04
GET /analytics
vet hypothesis

The API is designed around one constraint: a builder should be able to take a full swing at a hypothesis — real form, real marketplace, real revenue data — without filing a ticket or writing instrumentation code. The referrer_tag field is your experiment namespace.

This API follows the Vertical Platform Contract. Credit Cards, Mortgage, Banking, and Find a Financial Advisor expose the same four endpoints with the same shapes. Only the pocket field schema differs. See Vertical parity.

Quickstart

A working experiment in under 10 minutes.

bash — full lifecycle
# 1. Create a session
RESP=$(curl -s -X POST https://api.nerdwallet.com/v1/personal-loans/session \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"referrer_tag": "your_experiment"}')
SUB_ID=$(echo $RESP | jq -r '.sub_id')

# 2. Submit inputs
curl -s -X POST https://api.nerdwallet.com/v1/personal-loans/inputs \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"sub_id\":\"$SUB_ID\",\"fields\":{\"loan_amount\":15000,\"loan_purpose\":\"debt_consolidation\",\"credit_score_range\":\"700-749\",\"annual_income\":85000,\"employment_status\":\"employed\",\"state\":\"CA\"}}"

# 3. Render marketplace
curl -s "https://api.nerdwallet.com/v1/personal-loans/marketplace?sub_id=$SUB_ID&format=json" \
  -H "Authorization: Bearer YOUR_API_KEY"

# 4. Pull analytics
curl -s "https://api.nerdwallet.com/v1/personal-loans/analytics?referrer_tag=your_experiment&granularity=summary" \
  -H "Authorization: Bearer YOUR_API_KEY"

Authentication

All requests require a Bearer token from the NerdWallet Internal Developer Portal. Tokens are scoped to a vertical.

http header
Authorization: Bearer nw_pl_live_xxxxxxxxxxxxxxxxxxxx
Sandbox vs live. Tokens prefixed nw_pl_test_ return synthetic offers with no revenue attributed. Use nw_pl_live_ only when serving real users.

Base URL & versioning

base url
https://api.nerdwallet.com/v1/personal-loans/

The version segment is pinned in the URL. Breaking changes increment the version; non-breaking additions ship without a bump.

POSTCreate session

Generates a sub_id that ties form inputs, marketplace events, and revenue together. Call once per page load.

POST/session
Request body
FieldTypeRequiredDescription
referrer_tagstringrequiredExperiment namespace. All analytics aggregate here.e.g. "ryans_debt_consolidation_v2"
user_agentstringoptionalFor device-segmented analytics.
metadataobjectoptionalArbitrary key-value pairs. Max 10 keys, 256 chars each.e.g. {"variant": "control", "traffic_source": "email"}
Response
200 OK
400
401
json
{
  "sub_id": "plsn_8x4k2j9mrf",
  "referrer_tag": "ryans_debt_consolidation_v2",
  "expires_at": "2024-09-15T18:45:00Z",  // 4-hour TTL
  "created_at": "2024-09-15T14:45:00Z",
  "sandbox": false
}
{ "error": "INVALID_REFERRER_TAG", "message": "referrer_tag must match [a-z0-9_-]{3,64}" }
{ "error": "UNAUTHORIZED", "message": "Invalid or expired API key" }
TTL is 4 hours. Store sub_id in sessionStorage. New sessions after expiry still attribute to the same referrer_tag.

POSTSubmit inputs

Captures pocket field values. Can be called incrementally or in batch. Multiple calls merge.

POST/inputs
Request body
FieldTypeRequiredDescription
sub_idstringrequiredSession ID from POST /session.
fieldsobjectrequiredKey-value map of pocket fields. See pocket field schema.
json — example request
{
  "sub_id": "plsn_8x4k2j9mrf",
  "fields": {
    "loan_amount": 15000, "loan_purpose": "debt_consolidation",
    "credit_score_range": "700-749", "annual_income": 85000,
    "employment_status": "employed", "state": "CA",
    "birthdate": "1988-04-12", "housing_status": "rent"
  }
}
Response — 200 OK
json
{
  "sub_id": "plsn_8x4k2j9mrf",
  "fields_accepted": ["loan_amount", "loan_purpose", "credit_score_range", "annual_income", "employment_status", "state", "birthdate", "housing_status"],
  "fields_unknown": [],
  "match_readiness": "high",  // "high" | "partial" | "insufficient"
  "missing_required": []
}
match_readiness indicates whether the current field set is likely to return offers. Partial matches are valid for testing.

Pocket field schema

The full set of inputs the Personal Loans marketplace understands.

required conditional optional
loan_amount
integer · USD · 1,000–100,000
loan_purpose
enum · see values below
credit_score_range
enum · "300-579" through "800+"
annual_income
integer · USD · gross
employment_status
enum · employed / self_employed / retired / other
state
string · 2-letter ISO
birthdate
string · ISO 8601 · "YYYY-MM-DD"
housing_status
enum · own / rent / other
monthly_debt_payments
integer · USD
loan_term_months
integer · 12, 24, 36, 48, 60, 84
co_borrower
boolean · unlocks joint offers
citizenship_status
enum · citizen / perm_resident / visa
education_level
enum · high_school / bachelors / graduate / other
address_zip
string · 5-digit ZIP
loan_purpose enum values
valid values
"debt_consolidation"  "home_improvement"  "major_purchase"
"medical_expenses"    "moving"            "vacation"
"wedding"             "business"          "other"
credit_score_range enum values
valid values
"300-579"  "580-619"  "620-659"  "660-699"  "700-749"  "750-799"  "800+"

GETGet marketplace

Returns matched lender offers. Revenue attribution is automatic on click.

GET/marketplace
Query parameters
ParamTypeRequiredDescription
sub_idstringrequiredSession ID.
formatenumrequired"json" | "iframe" | "js"
max_offersintegeroptionalDefault: 5. Max: 20.
sortenumoptionalDefault: recommended."recommended" | "lowest_apr" | "highest_amount" | "nw_revenue"
Response — format: json
json
{
  "sub_id": "plsn_8x4k2j9mrf", "match_quality": "high",
  "offers": [{
    "nw_offer_id": "off_7a2k9x", "lender_name": "SoFi",
    "logo_url": "https://cdn.nerdwallet.com/logos/sofi.svg",
    "apr_range": { "min": 8.99, "max": 24.99 },
    "monthly_payment_estimate": 287, "nw_star_rating": 4.8,
    "features": ["no_origination_fee", "autopay_discount"],
    "prequalification": true,
    "click_url": "https://www.nerdwallet.com/go/sofi/pl?sub=plsn_8x4k2j9mrf&offer=off_7a2k9x"
  }],
  "total_offers": 7, "generated_at": "2024-09-15T14:47:33Z"
}
Preserve click_url. Contains the sub_id and nw_offer_id required for attribution. Do not construct your own URLs.

Render formats

jsonBuilder owns the UI. Full control, requires click_url preservation.

Returns structured offer objects. Build any UI you want.

iframeDrop-in embed. Zero rendering work, conversion tracking included.
html
<iframe src="https://widgets.nerdwallet.com/pl/embed?sub=plsn_8x4k2j9mrf&token=..." width="100%" height="680" frameborder="0"></iframe>
jsScript tag mount. Renders into any container.
iframe and js are the "full swing" path. The widget fires all conversion events internally. Use JSON only when you need custom UI control.

GETGet analytics

Query session and revenue data. Filter by referrer_tag, date range, or sub_id list.

GET/analytics
Query parameters
ParamTypeRequiredDescription
referrer_tagstringone ofFilter to one experiment.
sub_id_liststring[]one ofComma-separated. Max 500.
date_from / date_tostringoptionalISO dates. Defaults: 30 days ago → today.
granularityenumoptionalDefault: summary."row" | "daily" | "summary"
Response — summary
json
{
  "referrer_tag": "ryans_debt_consolidation_v2",
  "sessions_total": 1240, "marketplace_views": 842, "offer_clicks": 211,
  "click_rate": 0.251,
  "rev_attributed": 4820.00,  // same-day · preliminary
  "rev_confirmed": 3210.00,   // lender-confirmed · T+14 to T+45
  "rev_per_session": 2.59,     // primary comparison metric
  "top_lenders": [{ "lender": "SoFi", "clicks": 88, "rev_confirmed": 1340.00 }]
}

Revenue attribution model

rev_attributed
float · USD
Fires on click-through. Available same-day. Preliminary — use for early directional signals only.
rev_confirmed
float · USD
Settles when lender confirms a funded loan. Lags T+14 to T+45. This is the number to vet a hypothesis against.
rev_per_session
float · USD
rev_confirmed / sessions_total. Primary metric for comparing experiments.
Vet on rev_confirmed, not rev_attributed. Attribution rate and confirmation rate can diverge significantly by lender segment. Always wait for the confirmation lag before declaring a hypothesis vetted.

Error codes

  • 400INVALID_REFERRER_TAGMust match [a-z0-9_-]{3,64}. No uppercase, no spaces.
  • 400INVALID_FIELD_VALUEPocket field failed validation. Response includes fields_invalid array.
  • 400SUB_ID_EXPIREDSession exceeded 4-hour TTL. Create a new session.
  • 400INSUFFICIENT_INPUTSGET /marketplace called before required fields were submitted.
  • 401UNAUTHORIZEDAPI key missing, malformed, or expired.
  • 403VERTICAL_MISMATCHKey scoped to a different vertical. Personal Loans keys are prefixed nw_pl_.
  • 429RATE_LIMIT_EXCEEDEDCheck X-RateLimit-Reset header. Use exponential backoff.

Rate limits

500
requests/min per key
50
analytics queries/min
10k
sessions/day per key
100
analytics rows per page

Vertical parity contract

VerticalBase pathToken prefixRev modelStatus
Personal Loans/v1/personal-loans/nw_pl_CPL confirmedstable
Credit Cards/v1/credit-cards/nw_cc_CPCbeta
Mortgage/v1/mortgage/nw_mo_CPL confirmedbeta
Banking/v1/banking/nw_bk_CPC + pacingbeta
Find a Financial Advisor/v1/find-advisor/nw_fa_CPLplanned