Partner API

Carfax API reference

Order CARFAX history reports by VIN, track their status and download PDFs. Prepaid billing with a sandbox for testing.

Version 1. The shared OpenAPI 3.1 specification covers all products: https://api.notedcar.com/partner/v1/openapi.yaml.

Get an API key

Overview

The Partner API lets you order vehicle history reports by VIN and download them as PDF. Reports are generated asynchronously: you create an order, then either poll its status or receive a webhook when it is ready.

Billing is prepaid: every accepted order is charged against your account balance, and failed orders are refunded automatically.

https://api.notedcar.com/partner/v1

Authentication

Every request carries your API key as a bearer token. Keys are created in your dashboard and shown once. You may hold several active keys and rotate them without downtime; a key can be restricted to a list of source IP ranges. Keep keys server-side; never embed them in client applications.

Keys come in two modes. nc_live_… keys order real reports and spend balance. nc_test_… keys are sandbox keys: same account, same endpoints, deterministic answers, fixture PDFs, nothing charged and no data source called (see Sandbox below).

Authorization: Bearer nc_live_3kq9Zt7Hc2mVbL8xRwQp4nY6sDfG1aJeUoT5iKzM

Conventions

JSON in, JSON out (Content-Type: application/json). Timestamps are ISO 8601 in UTC. Money is in integer minor units of your account currency: kopiyky for UAH (the default), cents for USD; field names use _cents for both.

VINs are 17 characters; the letters I, O and Q are not used. Lowercase is accepted and normalised. language is en (English) or uk (Ukrainian). Ids carry a product prefix: history reports hr_…, top-ups top_…, ledger entries led_…, webhook events evt_….

Errors

Errors share one envelope with a stable machine-readable code; details names the field or state when there is one.

{
  "error": {
    "code": "insufficient_balance",
    "message": "Account balance is too low for this order.",
    "details": { "required_cents": 4500, "balance_cents": 1200 }
  }
}
HTTPcodeMeaning
400invalid_requestMalformed body, bad VIN or language, or an Idempotency-Key reused with a different body
401unauthorizedMissing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist
402insufficient_balanceBalance plus credit limit cannot cover the order
404not_foundUnknown id
422report_unavailableNo history report exists for this VIN; nothing charged
429rate_limitedAvailability checks or top-up creation; see Retry-After
503provider_unavailableUpstream data source or payment provider temporarily unavailable; retry later

Rate limits

Orders, status polling and billing endpoints are not rate-limited. Two calls are: GET /history-reports/availability (120 per minute per key, because a never-seen VIN reaches the upstream data source) and POST /topups (10 per minute per account, because each opens a payment at the bank or reserves an on-chain amount).

Availability responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds); a 429 includes Retry-After in seconds.

Idempotency

Send an Idempotency-Key header (any unique string up to 128 characters) with POST /history-reports. If the same key is sent again within 24 hours with the same body, you receive the original response and are not charged twice. The same key with a different body returns 400 invalid_request.

How orders are priced

You pay for a VIN once. The first order for a VIN costs report_cents of your current tier; every later order for the same VIN is free, in any language, as long as the first one succeeded (failed orders are refunded and do not count).

A report is a CARFAX snapshot dated by data_as_of, the “information available as of …” date printed on it. The data source keeps one snapshot per VIN and does not refresh it, so data_as_of can be earlier than the day you order, and ordering the VIN again returns the same data.

Tiers are marginal and reset monthly (UTC): an order’s price is the tier that month_to_date_volume + 1 falls into, counting paid orders only. Every response that creates or shows an order includes price_cents.

Account

GET/account

Your account, balance, price list and limits

pricing and limits are keyed by product, so later products add a key rather than change the existing ones. mode is the mode of the key you called with.

Response

{
  "id": "acc_01J9R5…",
  "name": "Example Motors",
  "status": "active",
  "currency": "UAH",
  "balance_cents": 1250000,
  "credit_limit_cents": 0,
  "pricing": {
    "history_reports": {
      "month_to_date_volume": 1340,
      "tiers": [
        { "from_monthly_volume": 0,    "report_cents": 4500 },
        { "from_monthly_volume": 1001, "report_cents": 4000 }
      ]
    }
  },
  "limits": { "history_reports": { "availability_checks_per_minute": 120 } },
  "topups": {
    "methods": ["monobank", "crypto_direct"],
    "min_cents": 10000,
    "max_cents": 20000000,
    "crypto_networks": [ { "id": "base", "name": "Base", "currencies": ["USDT", "USDC"] } ]
  },
  "webhook": { "url": "https://example.com/hooks/notedcar", "enabled": true },
  "mode": "live"
}

Responses

  • 200OK
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).

History reports

GET/history-reports/availabilityRate-limited: 120 requests per minute per key.

Free check whether a report can be produced for a VIN

Call this before ordering if you want to avoid 422 report_unavailable; it is not required. already_ordered is true when you have a ready report for this VIN; data_as_of is then its CARFAX data date and price_cents is 0.

Query parameters

vin*string17-character VIN

Response

{
  "vin": "1HGCM82633A004352",
  "available": true,
  "vehicle": "2003 HONDA ACCORD EX",
  "already_ordered": true,
  "data_as_of": "2026-08-04T09:33:59Z",
  "price_cents": 0
}

Responses

  • 200OK (available: false when the data source has no report)
  • 400invalid_request — malformed VIN
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).
  • 429rate_limited
  • 503provider_unavailable
POST/history-reports

Order a report

The charge is made when the order is accepted. If generation fails, the order moves to failed and the charge is refunded within the same operation.

201 Created with status: "ready" when the report can be delivered immediately (download_url is set). 202 Accepted with status: "queued" otherwise: poll GET /history-reports/{id} or wait for the webhook. Typical generation time is 20–60 seconds.

Headers

Idempotency-Keystring ≤ 128Optional. Same key + same body within 24 h replays the original response.

Body

vin*string17-character VIN
languageen | ukDefault en
client_referencestring ≤ 128Optional, echoed back and included in webhooks

Request

{
  "vin": "1HGCM82633A004352",
  "language": "en",
  "client_reference": "order-88213"
}

Response

{
  "id": "hr_01J9R5X3M6Q7V8W9Y0Z1A2B3C4",
  "vin": "1HGCM82633A004352",
  "language": "en",
  "status": "queued",
  "sandbox": false,
  "price_cents": 4500,
  "client_reference": "order-88213",
  "vehicle": "2003 HONDA ACCORD EX",
  "created_at": "2026-10-08T10:15:02Z",
  "ready_at": null,
  "data_as_of": null,
  "download_url": null,
  "download_url_expires_at": null,
  "failure_code": null
}

Responses

  • 201Ready immediately
  • 202Accepted and queued
  • 400invalid_request
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).
  • 402insufficient_balance
  • 422report_unavailable — nothing charged
  • 503provider_unavailable
POST/history-reports/batch

Order up to 100 reports in one call

Each item is accepted or refused on its own; the response is always 200 and lists one result per item in the same order, with the HTTP status that item would have received alone.

Differences from single orders: the availability pre-check is skipped so the call stays fast, which means a VIN with no report is accepted, then fails with vin_not_found and is refunded; vehicle is null on accepted items until the report is ready; a duplicate vin + language inside one batch is refused; Idempotency-Key is not supported.

Body

items*array (1–100)Objects with vin (required), language, client_reference

Request

{
  "items": [
    { "vin": "1HGCM82633A004352", "language": "en", "client_reference": "o-1" },
    { "vin": "5UX23EU01T9107676" }
  ]
}

Response

{
  "data": [
    { "index": 0, "status": 202, "order": { "id": "hr_…", "status": "queued" } },
    { "index": 1, "status": 402, "error": { "code": "insufficient_balance", "message": "…" } }
  ]
}

Responses

  • 200One result per item
  • 400invalid_request — items missing, empty or over 100
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).
GET/history-reports/{id}

Order status and download link

When status is ready, data_as_of is the date printed on the report (“information available as of …”, when CARFAX data for this VIN was pulled) and download_url is a signed link to the PDF valid for one hour; call this endpoint again for a fresh link. Report files are retained, so a link can always be regenerated.

Status values: queued, processing, ready, failed. Failure codes: vin_not_found, render_failed, provider_error. All of them are refunded.

Response

{
  "id": "hr_01J9R5X3M6Q7V8W9Y0Z1A2B3C4",
  "vin": "1HGCM82633A004352",
  "language": "en",
  "status": "ready",
  "sandbox": false,
  "price_cents": 4500,
  "client_reference": "order-88213",
  "vehicle": "2003 HONDA ACCORD EX",
  "created_at": "2026-10-08T10:15:02Z",
  "ready_at": "2026-10-08T10:15:41Z",
  "data_as_of": "2026-10-01T15:49:32Z",
  "download_url": "https://…",
  "download_url_expires_at": "2026-10-08T11:15:41Z",
  "failure_code": null
}

Responses

  • 200OK
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).
  • 404not_found
GET/history-reports

List your orders, newest first

Cursor pagination: pass the previous response’s next_cursor to continue.

Query parameters

vinstringFilter by VIN
statusstringqueued, processing, ready or failed
client_referencestringExact match
created_afterISO 8601
created_beforeISO 8601
limitintegerDefault 50, max 200
cursorstringFrom next_cursor

Response

{
  "data": [ { "id": "hr_…", "vin": "…", "status": "ready" } ],
  "next_cursor": "eyJpZCI6…"
}

Responses

  • 200OK
  • 400invalid_request — bad timestamp or cursor
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).

Top-ups

POST/topupsRate-limited: 10 requests per minute per account.

Add funds to your balance

Returns the payment to complete; the balance is credited once the provider confirms, usually within seconds for card and within a minute or two on-chain. A topup.paid webhook is sent when it lands.

For crypto_direct there is no payment_url; crypto_payment holds what to send and where: { network, network_name, currency, amount, address, qr_svg, expires_at, expired }. Send exactly amount (the trailing decimals identify your order) before expires_at, 30 minutes after creation.

Statuses: pending → processing (payment seen, crediting) → paid; or payment_failed (declined, expired or cancelled; nothing credited).

Body

amount_cents*integerIn your account currency; bounds in GET /account → topups
payment_methodmonobank | crypto_directDefault monobank (card). Only methods listed in GET /account → topups.methods are accepted
payment_networkstringcrypto_direct only, one of topups.crypto_networks[].id
payment_currencystringcrypto_direct only, one of that network’s currencies
return_urlhttps URLOptional, where the card payment page sends the payer afterwards

Request

{ "amount_cents": 50000, "payment_method": "monobank" }

Response

{
  "id": "top_01J9R7A1B2C3D4E5F6G7H8J9K0",
  "status": "pending",
  "amount_cents": 50000,
  "currency": "UAH",
  "payment_method": "monobank",
  "payment_url": "https://pay.monobank.ua/…",
  "charged_amount": null,
  "charged_currency": null,
  "crypto_payment": null,
  "return_url": "https://notedcar.com/api/dashboard/topups?topup=top_01J9R7…",
  "created_at": "2026-10-08T12:00:00Z",
  "paid_at": null,
  "failure_reason": null
}

Responses

  • 201Top-up created, payment pending
  • 400invalid_request — amount out of bounds, unknown method, non-https return_url
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).
  • 429rate_limited
  • 503provider_unavailable — the provider refused the order, or no exchange rate yet for a USD account
GET/topups/{id}

Top-up status

Poll this while a payment is open. For a crypto top-up it also triggers a chain scan.

Response

{
  "id": "top_01J9R7A1B2C3D4E5F6G7H8J9K0",
  "status": "pending",
  "amount_cents": 50000,
  "currency": "UAH",
  "payment_method": "monobank",
  "payment_url": "https://pay.monobank.ua/…",
  "charged_amount": null,
  "charged_currency": null,
  "crypto_payment": null,
  "return_url": "https://notedcar.com/api/dashboard/topups?topup=top_01J9R7…",
  "created_at": "2026-10-08T12:00:00Z",
  "paid_at": null,
  "failure_reason": null
}

Responses

  • 200OK
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).
  • 404not_found
GET/topups

Your top-ups, newest first

Same cursor pagination as /history-reports.

Query parameters

limitintegerDefault 50, max 200
cursorstringFrom next_cursor

Responses

  • 200OK
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).
POST/topups/{id}/claim

Claim an on-chain payment by transaction hash

For a crypto_direct top-up you paid after it stopped being watched, or when you do not want to wait for the next scan. Answers 200 with state: "paid", or a waiting state (not_found, unconfirmed, unavailable) to poll through.

Body

tx_hash*stringThe transaction hash / signature

Request

{ "tx_hash": "0x…" }

Response

{ "state": "paid", "topup": { "id": "top_…", "status": "processing" } }

Responses

  • 200Paid, or a waiting state
  • 400invalid_request — not a crypto_direct top-up
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).
  • 404not_found
  • 422invalid_request with details.state saying why the transaction does not pay for this top-up

Billing

GET/ledger

Balance history, newest first

Same cursor pagination as /history-reports. kind is one of topup, charge, refund, adjustment; charges are negative; each entry names the product it belongs to.

Query parameters

limitintegerDefault 50, max 200
cursorstringFrom next_cursor

Response

{
  "data": [
    { "id": "led_…", "kind": "charge", "amount_cents": -4500, "balance_after_cents": 1250000,
      "product": "history_reports", "history_report_id": "hr_…",
      "description": "History report 1HGCM82633A004352 (en)", "created_at": "2026-10-08T10:15:02Z" },
    { "id": "led_…", "kind": "topup", "amount_cents": 1000000, "balance_after_cents": 1254500,
      "product": null, "reference": "monobank:PUB-20261001-AB12CD34", "created_at": "2026-10-01T08:00:00Z" }
  ],
  "next_cursor": null
}

Responses

  • 200OK
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).
GET/usage

Daily counts for a date range

At most 92 days per call, grouped by product. paid counts orders that were charged and not refunded.

Query parameters

from*YYYY-MM-DD
to*YYYY-MM-DD

Response

{
  "history_reports": {
    "days": [
      { "date": "2026-10-07", "ordered": 412, "ready": 405, "failed": 7, "paid": 355,
        "charged_cents": 1597500, "refunded_cents": 31500 }
    ]
  }
}

Responses

  • 200OK
  • 400invalid_request — bad dates or a range over 92 days
  • 401Missing, invalid, expired or revoked key, suspended account, or IP outside the key’s allowlist (unauthorized).

Webhooks

If a webhook URL is configured in your dashboard, we POST an event when an order finishes or a top-up lands. Event types are <product>.<event>: history_report.ready, history_report.failed, account.low_balance, topup.paid (data is the top-up object).

The signature is HMAC-SHA256 of "{timestamp}.{raw_body}" using your webhook secret, hex encoded, sent as X-NotedCar-Signature: sha256=… with X-NotedCar-Timestamp. Reject requests whose timestamp is more than five minutes old. Respond with any 2xx within 10 seconds; we retry up to 8 times over about six hours with increasing delays. Deliveries are at-least-once, so handle duplicates by event id.

{
  "id": "evt_01J9R6…",
  "type": "history_report.ready",
  "created_at": "2026-10-08T10:15:41Z",
  "data": { "id": "hr_…", "vin": "…", "status": "ready", "client_reference": "order-88213",
            "download_url": "https://…", "download_url_expires_at": "2026-10-08T11:15:41Z" }
}

// Verification (Node.js)
import crypto from "node:crypto";

export function verify(rawBody, headers, secret) {
  const ts = headers["x-notedcar-timestamp"];
  const sig = headers["x-notedcar-signature"];
  const expected = "sha256=" + crypto.createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Sandbox

Use an nc_test_… key to integrate without spending anything. No balance is charged and no data source is called. Orders carry "sandbox": true and are listed separately from live orders (a live key never sees them and vice versa); GET /usage and GET /ledger ignore them. Ready sandbox orders download a fixed sample report in the requested language; price_cents shows what the order would cost live. Webhooks are delivered and signed exactly as for live orders. Top-ups are not sandboxed.

The answer is decided by the last four characters of the VIN:

VIN ends withResult
0000422 report_unavailable
1111202 queued, then ready about five seconds later (webhook sent)
9999202 queued, then failed with vin_not_found about five seconds later (webhook sent)
anything else201 ready immediately

Quick start

Three calls from a terminal: check a VIN, order, poll until ready.

# 1. Check availability (free)
curl -s "https://api.notedcar.com/partner/v1/history-reports/availability?vin=1HGCM82633A004352" \
  -H "Authorization: Bearer $NOTEDCAR_KEY"

# 2. Order
curl -s -X POST https://api.notedcar.com/partner/v1/history-reports \
  -H "Authorization: Bearer $NOTEDCAR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-88213" \
  -d '{"vin":"1HGCM82633A004352","language":"en","client_reference":"order-88213"}'

# 3. Poll until status is ready, then download
curl -s https://api.notedcar.com/partner/v1/history-reports/hr_01J9R5X3M6Q7V8W9Y0Z1A2B3C4 \
  -H "Authorization: Bearer $NOTEDCAR_KEY"