# Credit Score API

Returns an applicant's FICO score from a soft credit inquiry.

- Transport: JSON over HTTPS
- Typical duration: 95-140 seconds
- One application per person. Re-running the same identity shortly after a
  completed application is rejected.
- Service prefix: `/api/apply`

## LLM documentation endpoints

- `GET /api/apply/docs.md` — this document (`text/markdown`)
- `GET /api/apply/llms.txt` — alias of this document
- `GET /docs.md` and `GET /llms.txt` — combined documentation for both APIs
- `GET /swagger` — interactive OpenAPI UI
- `GET /openapi.json` — OpenAPI schema

TruePeopleSearch skip-tracing documentation is at `/search/docs.md` and
`/search/llms.txt`.

The Swagger UI groups this route under **Credit Score**. It uses the shared
`ApiKeyAuth` scheme (`X-API-Key`) in the **Authorize** dialog; the endpoint
also accepts `Authorization: Bearer <key>` for compatibility.

## Authentication

Every `POST /api/apply` request must send the API key, either as:

- `x-api-key: <key>`
- `Authorization: Bearer <key>`

Missing or wrong key:

```json
401 { "success": false, "error": "Unauthorized" }
```

If the service itself has no key configured, it refuses to run:

```json
503 { "success": false, "error": "API authentication is not configured" }
```

Public routes (no key needed): `GET /`, `GET /docs`, `GET /docs.md`,
`GET /llms.txt`, `GET /health`, `GET /swagger`, `GET /openapi.json`,
`GET /api/apply/docs.md` and `GET /api/apply/llms.txt`.

## POST /api/apply

Runs one application and returns the score.

### Request body

Required:

- `firstName` (string)
- `lastName` (string)
- `dob` (string, `MM/DD/YYYY`)
- `street` (string)
- `city` (string)
- `state` (string, 2-letter code or full name)
- `zip` (string; a numeric JSON value is also accepted)

Optional:

- `ssn` (string, 9 digits, with or without dashes) — submitted only if the
  application asks for it
- `email` (string) — generated from the name if omitted
- `phone` (string, 10 digits) — area code matched to the state if omitted
- `flow` (string, `fast` or `ui`) — overrides the server's `FLOW_MODE`

### Response

A score was retrieved (`score` is an integer, 300-850; approval is a separate
matter):

```json
200 { "success": true, "score": 764 }
```

The credit file is frozen, so nothing could be evaluated:

```json
200 { "success": true, "isFrozen": true }
```

The application failed without a score:

```json
502 { "success": false, "error": "Application declined" }
```

The time limit was exceeded:

```json
504 { "success": false, "error": "Run exceeded 170000ms at phase \"esign\"" }
```

Invalid input:

```json
400 { "success": false, "error": "Missing required fields: firstName, lastName, street, city, state, zip." }
```

Unexpected server failure:

```json
500 { "success": false, "error": "Internal Server Error" }
```

### Status codes

| Status | Meaning |
| --- | --- |
| `200` | Score retrieved, or the credit file is frozen |
| `400` | Invalid JSON, missing fields, invalid DOB, state or SSN |
| `401` | Missing or wrong API key |
| `502` | Application failed without a score |
| `503` | No API key is configured on the server |
| `504` | Run exceeded `RUN_DEADLINE_MS` |

The response never contains the applicant's name, address, date of birth, SSN,
email or phone. Error text is sanitized (`[email]`, `[phone]`, `[ssn]`,
`[date]`) and capped at 200 characters.

### Example

```bash
curl -s -o /tmp/apply.json -w 'HTTP %{http_code} in %{time_total}s\n' \
  -X POST "$BASE_URL/api/apply" \
  -H "x-api-key: $API_KEY" \
  -H 'Content-Type: application/json' \
  --max-time 190 \
  -d '{
    "firstName": "<first name>",
    "lastName": "<last name>",
    "dob": "<MM/DD/YYYY>",
    "street": "<street>",
    "city": "<city>",
    "state": "<2-letter state>",
    "zip": "<ZIP>",
    "ssn": "<9 digits>"
  }'

jq . /tmp/apply.json
```

Trailing slash is accepted directly: `POST /api/apply/`.

## GET /health

```json
200 { "status": "ok" }
```

## Notes

- `success: true` means a score was retrieved. A declined application with a
  score is still `success: true`.
- Declines without a score return `502`. Frozen files return `isFrozen: true`.
- A fresh identity is required for each run.
- The flow runs through a NodeMaven Scraping Browser session and closes it when
  the run ends, including on failure or timeout.
- `RECORD_RUN=true` writes per-run artifacts under `logs/`.
