> ## Documentation Index
> Fetch the complete documentation index at: https://docs.acrelens.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Endpoint reference

> Every public REST endpoint, request shape, and response shape.

Base URL: `https://api.acrelens.com/v1`

All endpoints except `GET /v1/health` require an `Authorization: Bearer al_(live|test)_...` header. See [authentication](/authentication).

## Reports

### `POST /v1/analyze`

Analyze a single property. Returns `202 Accepted` immediately and runs the analysis asynchronously.

**Request body**

| Field | Type | Required | Notes |
| - | - | - | - |
| `address` | string | ✅ | 1–500 chars |
| `state` | string | ✅ | 2-letter US state code |
| `mode` | string | ✅ | `off_grid`, `rural_residential`, `recreational`, `investment` |
| `county` | string | | 1–200 chars; improves regulation research |
| `acreage` | number | | Positive |
| `asking_price` | number | | Listing price in USD. Materially improves Investment-mode analysis (cap-rate, comp delta) and informs cost framing in other modes. |
| `lat` | number | | -90 to 90; provide both `lat` and `lng` to skip geocoding |
| `lng` | number | | -180 to 180 |
| `webhook_url` | string | | Valid URL, max 500 chars |
| `metadata` | object | | Up to 10 string-valued key-value pairs |
| `force_refresh` | boolean | | Defaults to `false`. When `true`, skips the per-customer 30-day report cache and forces a fresh analysis (charges normally). Use when the underlying data may have changed. |

**Headers**

| Header | Required | Notes | |
| - | - | - | - |
| `Authorization` | ✅ | \`Bearer al\_(live | test)\_...\` |
| `Content-Type` | ✅ | `application/json` | |
| `Idempotency-Key` | recommended | UUID — see [idempotency](/guides/idempotency) | |

**Response — `202 Accepted`**

```json theme={null}
{
  "report_id": "rpt_01HY2Z...",
  "status": "processing",
  "mode": "off_grid",
  "estimated_completion_seconds": 90,
  "poll_url": "https://api.acrelens.com/v1/reports/rpt_01HY2Z..."
}
```

**Response — `202 Accepted` (cache hit)**

If this customer analyzed the same `(address, state, mode)` within the last 30 days and that report completed successfully, the existing report is returned instead of running a new analysis — no charge, no quota burn. Pass `force_refresh: true` to bypass.

```json theme={null}
{
  "report_id": "rpt_01HY2Z...",
  "status": "completed",
  "mode": "off_grid",
  "cache_hit": true,
  "cached_from": "2026-04-20T22:01:30.000Z",
  "estimated_completion_seconds": 0,
  "poll_url": "https://api.acrelens.com/v1/reports/rpt_01HY2Z..."
}
```

**Errors**

| Code | HTTP | Cause |
| - | - | - |
| `unauthorized` | 401 | Missing / invalid / revoked key |
| `forbidden` | 403 | Account `CANCELED` or `PAST_DUE` |
| `insufficient_balance` | 402 | PAYG balance \< \$3.99 and no free reports left |
| `DAILY_CAP_EXCEEDED` | 402 | Daily spend cap hit |
| `MONTHLY_CAP_EXCEEDED` | 402 | Monthly spend cap hit |
| `idempotency_conflict` | 409 | Same key, different body |
| `validation_failed` | 422 | Body validation failed |
| `rate_limit_exceeded` | 429 | Too many req/sec |
| `quota_exceeded` | 429 | Monthly report limit reached (subscription accounts on the Free plan) |

***

### `GET /v1/reports`

List reports scoped to the calling customer, newest first. Free — no charge, no quota burn. Designed for polling integrations (Zapier "New Report Completed" trigger, batch syncs, dashboards).

**Query params**

| Param | Type | Notes |
| - | - | - |
| `status` | string | `pending`, `paid`, `processing`, `authorized`, `completed`, or `failed` |
| `mode` | string | `off_grid`, `rural_residential`, `recreational`, `investment` |
| `since` | string (ISO 8601) | Inclusive lower bound on `created_at` — "everything since" semantics. Primary mechanism for polling triggers. |
| `cursor` | string (ISO 8601) | Exclusive upper bound on `created_at` for backwards pagination. Pass the `next_cursor` from the previous response to walk further into history. |
| `limit` | integer | 1–100, default 50 |

**Response — `200 OK`**

```json theme={null}
{
  "data": [
    {
      "id": "rpt_01HY2Z...",
      "address": "123 Canyon Rd",
      "state": "NM",
      "county": "Taos",
      "mode": "off_grid",
      "status": "completed",
      "overall_score": 78,
      "acreage": 5.2,
      "created_at": "2026-04-28T22:00:00.000Z",
      "completed_at": "2026-04-28T22:01:30.000Z",
      "source": "api",
      "failed_reason": null
    }
  ],
  "next_cursor": "2026-04-20T09:18:00.000Z",
  "has_more": true
}
```

* `overall_score` is `null` until the report completes; `failed_reason` is `null` unless `status` is `failed`.
* `source` is the channel the report was created through (e.g. `api`, `dashboard`).
* `next_cursor` is the `created_at` of the oldest row returned when `has_more` is `true`; `null` otherwise. To walk further back, pass `cursor=<next_cursor>` on the next call.
* To poll for new reports, store the `created_at` of the latest item you've processed and pass it as `since`.

**Errors**

| Code | HTTP | Cause |
| - | - | - |
| `validation_failed` | 422 | Invalid query params (per-field messages in `fields`) |
| `unauthorized` | 401 | Auth failure |

***

### `GET /v1/reports/{id}`

Fetch a report. Returns the basic envelope while processing, the full body once `completed`.

**Response while processing — `200 OK`**

```json theme={null}
{
  "report_id": "rpt_01HY2Z...",
  "status": "processing",
  "mode": "off_grid",
  "created_at": "2026-04-28T22:00:00.000Z",
  "poll_url": "https://api.acrelens.com/v1/reports/rpt_01HY2Z..."
}
```

`status` will be one of `authorized`, `processing`, or `failed` while not yet complete.

**Response when completed — `200 OK`**

```json theme={null}
{
  "report_id": "rpt_01HY2Z...",
  "status": "completed",
  "mode": "off_grid",
  "property": {
    "address": "123 Canyon Rd, Taos, NM",
    "state": "NM",
    "county": "Taos",
    "lat": 36.4072,
    "lng": -105.5734,
    "acreage": 5.2,
    "asking_price": 65000
  },
  "scores": {
    "overall": 78,
    "sub_scores": {
      "solar": 92,
      "water": 64,
      "septic": 71,
      "building_codes": 80,
      "access": 83
    }
  },
  "confidence": {
    "solar": "high",
    "water": "medium",
    "septic": "medium",
    "building_codes": "high",
    "access": "high"
  },
  "summary": "Strong off-grid fundamentals: top-tier solar and accessible groundwater. The only material risk is unverified county-level rules on RV dwelling and alternative septic — a phone call to Taos County planning confirms.",
  "details": {
    "solar": {
      "score": 92,
      "summary": "6.3 peak sun hours daily and 2,140 kWh/kW annual production place this in the top tier for New Mexico.",
      "details": [
        "Peak sun hours: 6.3 hours/day",
        "Annual production: 2,140 kWh per installed kW",
        "Solar Access Law (N.M. Stat. §47-3-6) protects installation rights",
        "Recommended system size: 5–7 kW for typical off-grid load"
      ]
    },
    "water": {
      "score": 64,
      "summary": "Wells from 50–924 ft (avg 239 ft) across 10 nearby USGS sites — wide variability suggests a hydrogeological survey before drilling.",
      "details": [
        "Nearby wells: 10 (USGS Groundwater)",
        "Estimated drilling depth range: 150–500 ft",
        "Budget for a hydrogeological survey ($800–$1,500)"
      ]
    },
    "buildability": {
      "score": 71,
      "summary": "TCEQ governs septic (30 TAC §285); composting toilets allowed under NSF/ANSI 41 with local approval. RV-as-dwelling rules are local — verify with county.",
      "details": [
        "Adopted code: IRC 2018 (state default)",
        "Composting toilet status under NSF/ANSI 41: conditional",
        "RV-as-dwelling: verify with Taos County"
      ],
      "rv_living_allowed": "conditional",
      "composting_toilet_allowed": "conditional",
      "permit_difficulty": "moderate"
    },
    "access": {
      "score": 83,
      "summary": "Likely good accessibility via a county-maintained gravel road. Current condition requires on-site verification.",
      "details": [
        "Road classification: county-maintained gravel",
        "Distance to nearest paved road: approx. 0.4 mi",
        "Verify on site: current pavement condition, washouts, driveway grade"
      ]
    }
  },
  "considerations": [
    "Verify all county regulations with Taos County planning department",
    "Get well drilling quotes from local contractors",
    "Confirm road access and seasonal maintenance responsibilities"
  ],
  "estimated_costs": {
    "solar": { "low": 18000, "high": 30000, "notes": "5kW off-grid system with batteries" },
    "well": { "low": 7500, "high": 25000, "notes": "Based on estimated depth range" },
    "septic": { "low": 5000, "high": 22000, "notes": "Type-IV alternative system likely" },
    "road": { "low": 0, "high": 3000, "notes": "Minimal improvements likely needed" },
    "permits": { "low": 1000, "high": 3000, "notes": "County-specific fees" },
    "total_low": 31500,
    "total_high": 83000
  },
  "sources": [
    { "name": "NREL PVWatts v8", "url": "https://developer.nrel.gov/..." }
  ],
  "metadata": { /* whatever you sent */ },
  "generated_at": "2026-04-28T22:01:30.000Z"
}
```

The `details` object carries a per-topic breakdown — score, summary,
detail bullets, and (for `buildability`) regulatory extras like RV
living status, composting toilet status, and permit difficulty.

The `considerations` array surfaces actionable verification items the
buyer should follow up on before closing.

The `estimated_costs` object provides per-category cost ranges in USD,
plus `total_low` and `total_high` aggregates.

**Response when failed — `200 OK`**

```json theme={null}
{
  "report_id": "rpt_01HY2Z...",
  "status": "failed",
  "mode": "off_grid",
  "failed_reason": "Geocoding lookup failed for address."
}
```

Sub-score keys vary by mode — see [analysis modes](/concepts/analysis-modes).

**Errors**

| Code | HTTP | Cause |
| - | - | - |
| `not_found` | 404 | Report doesn't exist or belongs to another customer |
| `unauthorized` | 401 | Auth failure |

***

### `POST /v1/batch`

Submit 2–50 properties as a single request. See the [batch guide](/guides/batch-analysis).

**Request body**

| Field | Type | Required | Notes |
| - | - | - | - |
| `items` | array | ✅ | 2–50 items, each with the same fields as `POST /v1/analyze` |
| `delivery_mode` | enum | | `"per_item"` (default) or `"batch"` |
| `webhook_url` | string | | Per-batch override |
| `metadata` | object | | Inherited by items without their own |

**Response — `202 Accepted`**

```json theme={null}
{
  "batch_id": "bat_01HY2Z...",
  "status": "processing",
  "items_count": 3,
  "reports": [
    { "report_id": "rpt_a...", "status": "processing", "poll_url": "..." },
    { "report_id": "rpt_b...", "status": "processing", "poll_url": "..." },
    { "report_id": "rpt_c...", "status": "processing", "poll_url": "..." }
  ],
  "delivery_mode": "per_item",
  "estimated_completion_seconds": 120
}
```

***

## Account

### `GET /v1/balance`

Current balance, free reports remaining, and the last 10 transactions.

**Response — `200 OK`**

```json theme={null}
{
  "customer_id": "cust_01HY2Z...",
  "billing_mode": "PAYG",
  "balance_cents": 2599,
  "free_reports_remaining": 2,
  "transactions": [
    {
      "id": "txn_...",
      "type": "DEDUCTION",
      "amount_cents": -399,
      "balance_after_cents": 2599,
      "description": "Report charge (rpt_01HY2Z...)",
      "created_at": "2026-04-28T22:00:00.000Z"
    },
    {
      "id": "txn_...",
      "type": "TOPUP",
      "amount_cents": 5000,
      "balance_after_cents": 2998,
      "description": "Balance top-up via Stripe",
      "created_at": "2026-04-28T20:00:00.000Z"
    }
  ]
}
```

Transaction types: `TOPUP`, `DEDUCTION`, `REFUND`, `CREDIT`, `ADJUSTMENT`.

***

### `GET /v1/usage`

Period-bounded usage stats, quota, and rate.

**Response — `200 OK`**

```json theme={null}
{
  "customer_id": "cust_01HY2Z...",
  "tier": "DEVELOPER",
  "billing_mode": "PAYG",
  "balance_cents": 2599,
  "free_reports_remaining": 2,
  "period_start": "2026-04-01T00:00:00.000Z",
  "period_end": "2026-04-30T23:59:59.999Z",
  "quota_limit": 25,
  "used_this_period": 8,
  "remaining": 17,
  "overage_allowed": false,
  "overage_units": 0,
  "overage_price_per_unit_cents": 0,
  "payg_rate_per_report_cents": 399
}
```

| Field | Notes |
| - | - |
| `tier` | API tier (`DEVELOPER`, `STARTER`, `GROWTH`, `SCALE`, `ENTERPRISE`) |
| `quota_limit` | Reports allowed in the current period |
| `used_this_period` | Reports used so far this period |
| `remaining` | `quota_limit − used_this_period`, floored at 0 |
| `overage_allowed` | Whether usage beyond `quota_limit` is permitted (false for `DEVELOPER`) |
| `overage_units` | Reports used beyond `quota_limit`, floored at 0 |
| `overage_price_per_unit_cents` | Per-report overage price for the tier, in cents |

***

### `POST /v1/billing/portal`

Returns a Stripe Customer Portal URL where your end users (or you) can manage cards, view receipts, and update billing details.

**Request body** (optional)

```json theme={null}
{ "return_url": "https://yourapp.com/billing" }
```

**Response — `200 OK`**

```json theme={null}
{ "url": "https://billing.stripe.com/..." }
```

**Errors**

| Code | HTTP | Cause |
| - | - | - |
| `not_found` | 404 | Customer not yet linked to Stripe (no top-up yet) |
| `upstream_error` | 502 | Stripe is down |

***

## Reference data

### `GET /v1/states/{code}`

State-level land intelligence — the same regulatory, climate, and land-use data the reports use to ground their analysis.

**Path params**

| Param | Notes |
| - | - |
| `code` | 2-letter state code, e.g. `NM` |

**Query params**

| Param | Notes |
| - | - |
| `mode` | Optional. If provided, only that mode's data is returned. Otherwise, all four modes. |

**Response — `200 OK`**

```json theme={null}
{
  "state_code": "NM",
  "modes": {
    "off_grid": { /* mode-specific data */ },
    "rural_residential": { /* ... */ },
    "recreational": { /* ... */ },
    "investment": { /* ... */ }
  },
  "shared_facts": { /* state-wide facts */ }
}
```

Cached at the edge: `Cache-Control: public, max-age=3600`.

**Errors**

| Code | HTTP | Cause |
| - | - | - |
| `not_found` | 404 | State code not seeded |
| `validation_failed` | 422 | Invalid mode parameter |

***

### `GET /v1/health`

Liveness probe. **Unauthenticated** and not rate-limited.

**Response — `200 OK`**

```json theme={null}
{
  "status": "healthy",
  "version": "0.1.0",
  "timestamp": "2026-04-28T22:00:00.000Z"
}
```

Returns `503` with `internal_error` if the database or Redis is down.

***

## Common headers

Every response includes:

| Header | Notes |
| - | - |
| `x-request-id` | UUID — same value as `error.request_id` on failures. Quote when contacting support. |
| `x-acrelens-customer-id` | Your customer ID |
| `x-acrelens-api-key-id` | The ID of the API key used for this request |
| `x-ratelimit-limit` | Windowed rate-limit budget (rate/s × window seconds), not the per-second number — see [rate limits](/reference/rate-limits) |
| `x-ratelimit-remaining` | Requests remaining in the current window |
| `x-ratelimit-reset` | Unix timestamp (seconds) when the window resets |

`POST /v1/analyze` additionally sets:

| Header | Notes |
| - | - |
| `x-report-id` | The `report_id` for this request (also in the response body) |

On `429 rate_limit_exceeded`, also:

| Header | Notes |
| - | - |
| `retry-after` | Seconds to wait |

If the rate-limiter's backing store (Redis) is unavailable, the request still succeeds (fail-open) but the `x-ratelimit-*` headers are omitted and replaced with:

| Header | Notes |
| - | - |
| `x-ratelimit-status` | `degraded` — rate limiting could not be evaluated for this request |

See [rate limits](/reference/rate-limits) for retry behavior.

***

## Admin endpoints

The `/v1/prompt-versions` endpoints (CRUD on AI prompt versions) exist but are intended for administrators only. Reach out at [support@acrelens.com](mailto:support@acrelens.com) if you need access.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.