# FinNotes API Documents

This document defines the commercial FinNotes API contract for authentication, request format, point charging, response headers, product endpoints, usage inspection, logs, newsletter preferences, and AI Agent push delivery.

The canonical API base URL is:

```text
https://api.finnotes.com/v1
```

All examples use API version `v1`. The developer console (mint keys, inspect usage, configure delivery) lives at `https://platform.finnotes.com` — that hostname serves the human-readable HTML console, not the API itself.

## 1. Core Principles

FinNotes API access is point based. A normal product request is charged when it successfully returns or delivers billable data. Error and invalid requests are covered by the daily invalid-request allowance in Section 6.

Points are deducted from the authenticated account's active monthly allowance. Response headers and response bodies include the charged amount and remaining balance whenever the endpoint is billable.

All requests must use HTTPS and JSON. API keys must be sent in the `Authorization` header. Do not send API keys in query strings, request bodies, logs, or client-side code.

## 2. Plans And Monthly Points

| Plan | Monthly points | Sustained request limit | Intended usage |
| --- | ---: | ---: | --- |
| Pro | 1,350 pts | 3 requests / second | Regular API access and controlled production usage. |
| Max | 5,000 pts | 10 requests / second | Higher-volume workflows, broader data pulls, and heavier automation. |

Monthly points reset on the account billing reset date. Unused points do not roll over unless a separate commercial agreement says otherwise.

## 3. Authentication

Use Bearer authentication.

```http
Authorization: Bearer fnp_xxxxxxxxxxxxxxxxxxxxx
Accept: application/json
```

All API platform keys use the `fnp_` prefix. There is no separate live / sandbox / test key class — each account uses a single key type, matching the convention used by OpenAI, Anthropic, and Gemini. Developers test against their own real account; we do not maintain a sandbox environment.

Example:

```bash
curl "https://api.finnotes.com/v1/news?range=today&type=all" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Required Headers

| Header | Required | Description |
| --- | --- | --- |
| `Authorization` | Yes | `Bearer <api_key>`. |
| `Accept` | Recommended | Use `application/json`. |
| `Content-Type` | Required for body requests | Use `application/json` for `POST`, `PUT`, and `PATCH`. |
| `Idempotency-Key` | Recommended for writes | Unique client-generated key for retrying write requests safely. |

### Response Headers

Billable and usage-aware endpoints return point headers.

| Header | Description |
| --- | --- |
| `X-Request-Id` | Unique request identifier for support, logs, and reconciliation. |
| `X-Points-Charged` | Points charged for this request. `0.00` for non-billable or failed requests. |
| `X-Points-Remaining` | Remaining monthly points after this request. |
| `X-Points-Allowance` | Current monthly point allowance for the account. |
| `X-RateLimit-Limit` | Account-specific request limit for the current window, when applicable. |
| `X-RateLimit-Remaining` | Remaining requests in the current rate-limit window, when applicable. |
| `Retry-After` | Seconds to wait before retrying a `429` response. |

Point values are decimal numbers. The ledger stores point charges to two decimal places.

## 4. API Key Permissions

Permissions are configured in the API Platform UI when creating or editing a key.

| Permission | Default | Grants access to |
| --- | --- | --- |
| `Basic(all news, report & data)` | On | News metadata, market-news detail, chart-news detail, and data-series endpoints. |
| `Notes Download & Upload` | On | Notes list, download, import, upload, and sync endpoints for the authenticated user. |
| `View Usage and remaining points` | Off | Own point balance and current-month daily point totals. |
| `View Log` | Off | Read enabled request logs. |
| `Manage Log` | Off | Enable, disable, and delete request logs. |
| `Manage Newsletter preferences` | Off | Create, update, and cancel report/push subscriptions. |
| `All API usage state` | Off | Account-wide current-month daily point totals across all keys. This does not expose request details. |
| `Notification management` | Off | Manage API activity email notifications. |

Every billable response still returns the calling key's point headers. The `View Usage and remaining points` permission is required only when calling dedicated account or usage endpoints.

### 4.1 View The Current Key's Permissions

```http
GET /v1/api-keys/current
```

Returns the permissions attached to the API key used for the request.

Required permission: none beyond a valid API key. A key must always be able to inspect its own permissions so developers can debug access issues without needing account-wide Usage or Log permissions.

Point charge: `0.00 pts`. Key inspection is free.

This endpoint does not list other API keys and does not return the full API key secret.

**Point balance.** The envelope's `remaining_points` field reflects the live account balance — for a brand-new key that has not yet made a billable request, it returns the full plan allowance (e.g. `1350.0` for Pro). For the authoritative full point breakdown (allowance, used, remaining, usage rate, reset timestamp), call **`/v1/account/points`**.

### Request

```bash
curl "https://api.finnotes.com/v1/api-keys/current" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

```json
{
  "data": {
    "api_key_id": "key_prod_001",
    "name": "Production server",
    "key_prefix": "fnp",
    "status": "active",
    "permissions": [
      {
        "id": "basic",
        "label": "Basic(all news, report & data)",
        "enabled": true
      },
      {
        "id": "notes",
        "label": "Notes Download & Upload",
        "enabled": true
      },
      {
        "id": "usage",
        "label": "View Usage and remaining points",
        "enabled": true
      },
      {
        "id": "view_log",
        "label": "View Log",
        "enabled": false
      }
    ]
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_8e30d3ac"
}
```

## 5. Point Charging Rules

### 5.1 News List Metadata

`GET /v1/news` returns routing metadata and display metadata for `market-news` and `chart-news`: `id`, `type`, `slug`, `url`, `title`, and `dek`.

| Date mode | Request | Point charge |
| --- | --- | ---: |
| Today | `range=today` | 1.00 pt |
| Specific date | `date=YYYY-MM-DD` | 1.50 pts |
| Last X days | `last_days=X` | `1.5 * X - 0.5` pts |

Examples:

| Example | Charge |
| --- | ---: |
| `GET /v1/news?range=today&type=all` | 1.00 pt |
| `GET /v1/news?date=2026-06-19&type=all` | 1.50 pts |
| `GET /v1/news?last_days=7&type=all` | 10.00 pts |

The `type` filter does not multiply the point charge. A request for `type=all` and a request for a single supported type use the same date-window charge. The list endpoint does not include article body content.

Use the returned `type` and `slug` to request a priced detail payload with `GET /v1/news/{type}/{slug}`. The returned website `url` remains available for display, attribution, and user-facing links, but clients should not URL-encode the full website URL into the detail endpoint.

### 5.2 Product Request Pricing

| Product | Endpoint family | Payload | Point charge |
| --- | --- | --- | ---: |
| Market-news detail | `/v1/news/market-news/{slug}` | Single market-news article payload. | 1.00 pt |
| Chart-news detail | `/v1/news/chart-news/{slug}` | Chart article payload and chart binding metadata. | 1.25 pts |
| Today full bundle | `/v1/news/today/full` | Every today's published article with full body + references + chart (chart-news only). | Sum: `1.00 × N(market-news) + 1.25 × N(chart-news)`. 0 pt if empty. |
| Data-series list | `/v1/data-series/categories`, `/v1/data-series` | Category directory or series directory. Category items return `name`, `slug`, and `url`; series items return `name`, `slug`, `url`, and `unit`. | First 10 list requests per day are free; then 5.00 pts/request |
| Data-series detail | `/v1/data-series/{slug}` | Detailed metadata for one series. Does not include data points or latest numeric value. | 1.00 pt |
| Data-series points | `/v1/data-series/{slug}/points` | Selected data points for one series. Supports all, between dates, or since a date. | 1.00 pt per 250 returned data points, minimum 1.00 pt |
| Own Notes list | `/v1/notes` | User-owned Notes list for the authenticated account. | First 10 list requests per day are free; then 1.00 pt/request |
| Notes download / upload | `/v1/notes/{note_id}`, `/v1/notes/import` | Move Notes payloads in or out of the account workspace. | 3.50 pts |

### 5.3 Non-Billable Platform Requests

The following requests do not deduct points:

| Area | Examples |
| --- | --- |
| Current API key inspection | `/v1/api-keys/current` |
| Account balance and usage inspection | `/v1/account/points`, `/v1/usage` |
| Log configuration and retrieval | `/v1/logs/settings`, `/v1/logs` |
| Newsletter preference configuration | `/v1/newsletter/preferences`, `/v1/push/*/preferences` |
| API activity notification settings | `/v1/notifications/preferences` |

These endpoints may require explicit key permissions.

### 5.4 Push Charging

Free report subscriptions do not consume points.

Trace Data Release push is charged when a selected release or policy decision alert is delivered:

| Delivery channel | Charge per triggered alert |
| --- | ---: |
| Email only | 2.50 pts |
| API Push only | 2.50 pts |
| Email + API Push | 4.00 pts |

Real-time News Push follows the same point price as actively retrieving the delivered article through the API. For example, a delivered `market-news` item costs 1.00 pt and a delivered `chart-news` item costs 1.25 pts.

Push charges are deducted at delivery time, not at preference-save time.

## 6. Abuse Protection And Rate Limits

FinNotes applies invalid-request charging and per-second request limits to reduce abuse, scraping probes, and unfair competitive traffic.

### Daily Invalid-Request Allowance

Each membership account receives 40 free error or invalid requests per calendar day. The allowance resets daily at 00:00 UTC.

After the first 40 error or invalid requests in the same day, each additional error or invalid request costs `1.00 pt`.

Examples of error or invalid requests:

| Request type | Example |
| --- | --- |
| Malformed query | Invalid date format, unsupported enum value, missing required range parameter. |
| Invalid resource identifier | Wrong article slug, wrong data-series slug, nonexistent note ID. |
| Permission error | API key calls an endpoint without the required permission. |
| ~~Authentication error (401)~~ | **NOT counted** — a missing / malformed / revoked / invalid key is anonymous (un-attributable to an account), so a `401` never consumes the allowance nor deducts points. Only requests that authenticate to a known account and then fail (`400` / `403` / `404` / `422`) count toward the 40. |
| Validation error | JSON body is valid JSON but fails semantic validation. |

The invalid-request allowance is account-level, not per API key. Requests across all API keys on the same account count toward the same daily 40-request allowance.

### Invalid-Request Charging Rules

| Daily invalid request count | Point charge |
| ---: | ---: |
| 1-40 | 0.00 pts |
| 41+ | 1.00 pt per invalid request |

Server-side `500` and `503` errors are not counted as user invalid requests and do not deduct points.

Rate-limit responses (`429`) do not deduct points. Clients should respect `Retry-After`.

**No-overdraw policy.** Past the 40 free invalid requests, if the account's `remaining_points` is less than the 1.00 pt overage charge, FinNotes **does not deduct anything**. The 4xx error response still goes back to the caller and the daily invalid counter still increments (so abuse signals are preserved), but no points are debited and no `X-Points-Charged` header is emitted on that response. This is forgiving by design — the most common cause of an excess-of-40 invalid burst is an AI agent that hasn't realised the account is out of quota and is retrying.

### Per-Second Request Limits

| Plan | Limit |
| --- | ---: |
| Pro | 3 requests / second |
| Max | 10 requests / second |

The rate limit is account-level and applies across all API keys. If a client exceeds the plan limit, FinNotes returns `429 rate_limit_exceeded` with `Retry-After`.

The rate-limit algorithm is a **1-second sliding window of completed requests**: each successful request lands a row in the internal request ledger with its completion timestamp; a new request is rejected when the count of ledger rows in the trailing 1.0 second already meets the plan limit. `Retry-After` returns the integer seconds until the oldest in-window row expires (typically `1`).

Practical note for clients: because the window counts *completed* requests rather than arrival timestamps, a tight burst of concurrent requests can briefly let more than the nominal plan limit start processing — the limit re-asserts as soon as the first wave of ledger rows is written. Throttling on the client side (one request at a time, or a small concurrency cap) gives the cleanest behaviour. There is no token-bucket reserve.

### Error Response With Invalid-Request Charge

When an invalid request is charged after the daily free allowance is exhausted, the error response includes point fields:

```json
{
  "error": {
    "code": "not_found",
    "message": "The requested data series does not exist.",
    "request_id": "req_invalid_41",
    "details": {
      "invalid_requests_today": 41,
      "free_invalid_requests_remaining": 0
    }
  },
  "points_charged": 1.0,
  "remaining_points": 916.0
}
```

## 7. Common Request Parameters

### Article Type Enum (Case Convention)

The API uses kebab-case for article type values in both request and response:

| API value | Internal storage |
| --- | --- |
| `market-news` | `market_news` |
| `chart-news` | `chart_news` (resolved via the `ChartArticle` model) |

The internal snake_case form is never exposed in v1 API payloads. Clients must use the kebab-case form for filtering, path parameters, and event payloads.

### Date And Time

All API timestamps are ISO 8601 strings in UTC, for example:

```text
2026-06-19T10:30:00Z
```

Date-only parameters use `YYYY-MM-DD` and are interpreted as the FinNotes publishing date in UTC.

### Pagination

List endpoints use cursor pagination.

| Parameter | Type | Default | Description |
| --- | --- | ---: | --- |
| `limit` | integer | 50 | Number of records to return. Maximum `100`. |
| `cursor` | string | None | Cursor returned by the previous page. |

Paginated responses include:

```json
{
  "data": [],
  "pagination": {
    "next_cursor": "cur_abc123",
    "has_more": true
  }
}
```

### Sorting

Where sorting is supported:

| Parameter | Values | Description |
| --- | --- | --- |
| `sort` | `published_at`, `score`, `title` | Sort field. |
| `order` | `asc`, `desc` | Sort direction. Defaults to `desc`. |

## 8. Standard Response Envelope

Successful billable endpoints use this shape:

```json
{
  "data": {},
  "points_charged": 1.0,
  "remaining_points": 921.0,
  "request_id": "req_9f8a21c7"
}
```

Successful list endpoints use:

```json
{
  "data": [],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  },
  "points_charged": 1.0,
  "remaining_points": 921.0,
  "request_id": "req_9f8a21c7"
}
```

Non-billable platform endpoints use the same envelope but return `points_charged: 0.0`.

## 9. Error Response

Errors use a consistent JSON shape:

After the daily 40-request invalid-request allowance is exhausted, user-side error responses may include `points_charged: 1.0` and `remaining_points` at the top level.

```json
{
  "error": {
    "code": "insufficient_points",
    "message": "The account does not have enough points for this request.",
    "request_id": "req_9f8a21c7",
    "details": {
      "required_points": 5.5,
      "remaining_points": 2.0
    }
  }
}
```

Every error response carries the same `X-Request-Id` HTTP header documented in section 3, regardless of status code. The `request_id` field inside the error body mirrors that header so support tooling can grep either surface.

| HTTP status | Code examples | Meaning |
| ---: | --- | --- |
| `400` | `bad_request`, `invalid_query` | Malformed request or incompatible parameters. |
| `401` | `unauthorized`, `invalid_api_key` | Missing, invalid, or revoked API key. |
| `402` | `insufficient_points` | Request is valid but the account does not have enough points. |
| `403` | `permission_denied`, `membership_inactive` | API key lacks the required permission, or its owner no longer holds an active Pro or Max membership. Renew the membership to reactivate the key. |
| `404` | `not_found` | Requested resource does not exist or is not visible to the account. |
| `409` | `idempotency_conflict` | Same idempotency key was reused with a different request body. |
| `422` | `validation_error` | Request body is syntactically valid but semantically invalid. |
| `429` | `rate_limit_exceeded` | Too many requests. Respect `Retry-After`. |
| `500` | `internal_error` | Internal server error. |
| `503` | `service_unavailable` | Temporary outage or maintenance. |

## 10. Endpoints

## 10.1 News List Metadata

```http
GET /v1/news
```

Returns metadata for `market-news` and `chart-news`.

Required permission: `Basic(all news, report & data)`.

Point charge: by date mode.

### Query Parameters

Exactly one date mode must be supplied.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `range` | string | Conditional | Use `today` for the current FinNotes publishing date. |
| `date` | string | Conditional | Specific publishing date, `YYYY-MM-DD`. |
| `last_days` | integer | Conditional | Rolling lookback window in days. Minimum `1`. |
| `type` | string | Optional | `all`, `market-news`, or `chart-news`. Defaults to `all`. |
| `limit` | integer | Optional | Page size. Maximum `100`. |
| `cursor` | string | Optional | Cursor for the next page. |
| `sort` | string | Optional | `published_at` or `score`. Defaults to `published_at`. |
| `order` | string | Optional | `asc` or `desc`. Defaults to `desc`. |

Invalid combinations:

| Invalid request | Reason |
| --- | --- |
| `range=today&date=2026-06-19` | Multiple date modes. |
| `last_days=0` | Lookback window must be at least one day. |
| `type=report` | Unsupported news list type. |

### Request

```bash
curl "https://api.finnotes.com/v1/news?range=today&type=all&limit=50" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

```json
{
  "data": [
    {
      "id": "mn_01jz9x2h0n",
      "type": "market-news",
      "slug": "oil-risk-premium-cools",
      "title": "Oil trims weekly gain as shipping risk premium cools",
      "url": "https://finnotes.com/market-news/oil-risk-premium-cools",
      "dek": "Crude eased after traders reassessed near-term supply disruption risk.",
      "category": "commodity",
      "published_at": "2026-06-19T08:15:00Z",
      "importance_score": 7.4,
      "anomaly_score": 5.8
    }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  },
  "points_charged": 1.0,
  "remaining_points": 921.0,
  "request_id": "req_9f8a21c7"
}
```

Each list item carries a `category` field — the slug of the article's primary FinNotes Category (e.g. `"macro"`, `"policy"`, `"commodity"`, `"china"`, `"company"`, `"industry"`, `"europe"`, `"strategy"`, `"region"`). It is `null` when the article has not been assigned a category. The `category` field is also returned by the detail endpoints (§10.2) and by the bundled `/v1/news/today/full` endpoint (§10.4).

## 10.2 News Detail By Type And Slug

```http
GET /v1/news/{type}/{slug}
```

Returns the full payload for one priced news-detail product.

This is the primary detail endpoint because it follows the same routing information returned by `GET /v1/news`. A list item with:

```json
{
  "type": "market-news",
  "slug": "oil-risk-premium-cools",
  "url": "https://finnotes.com/market-news/oil-risk-premium-cools"
}
```

maps directly to:

```http
GET /v1/news/market-news/oil-risk-premium-cools
```

Required permission: `Basic(all news, report & data)`.

Point charge:

| Type | Point charge | Detail support |
| --- | ---: | --- |
| `market-news` | 1.00 pt | Supported |
| `chart-news` | 1.25 pts | Supported |

### Path Parameters

| Parameter | Description |
| --- | --- |
| `type` | `market-news` or `chart-news`. |
| `slug` | Article slug returned by `/v1/news`. This is the final path segment of the website URL. |

### Query Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `include_chart` | boolean | `true` | Chart-news only. Include chart binding metadata. Ignored for market-news. |
| `include_references` | boolean | `true` | Include references and source links where available. |

### Market-News Request

```bash
curl "https://api.finnotes.com/v1/news/market-news/oil-risk-premium-cools?include_references=true" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Market-News Response

```json
{
  "data": {
    "id": "mn_01jz9x2h0n",
    "type": "market-news",
    "slug": "oil-risk-premium-cools",
    "title": "Oil trims weekly gain as shipping risk premium cools",
    "url": "https://finnotes.com/market-news/oil-risk-premium-cools",
    "dek": "Crude eased after traders reassessed near-term supply disruption risk.",
    "body": "Full article body...",
    "category": "commodity",
    "published_at": "2026-06-19T08:15:00Z",
    "importance_score": 7.4,
    "anomaly_score": 5.8,
    "references": [
      {
        "title": "Source title",
        "url": "https://example.com/source"
      }
    ]
  },
  "points_charged": 1.0,
  "remaining_points": 920.0,
  "request_id": "req_6b18e827"
}
```

### Chart-News Request

```bash
curl "https://api.finnotes.com/v1/news/chart-news/us-yields-reprice-policy-path?include_chart=true" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Chart-News Response

```json
{
  "data": {
    "id": "cn_01jz9xd7nm",
    "type": "chart-news",
    "slug": "us-yields-reprice-policy-path",
    "title": "US yields reprice the policy path after payrolls",
    "url": "https://finnotes.com/charts/us-yields-reprice-policy-path",
    "dek": "A chart-backed read on the front-end rate reaction.",
    "body": "Full article body...",
    "category": "macro",
    "published_at": "2026-06-19T09:05:00Z",
    "importance_score": 8.1,
    "anomaly_score": 7.2,
    "chart": {
      "id": "chart_10y_2y_policy_path",
      "title": "Treasury curve and policy expectations",
      "is_featured": true,
      "window_type": "dynamic",
      "dynamic_window_months": 36,
      "window_start_date": null,
      "window_end_date": null,
      "series": [
        {
          "series_id": "us_10y_yield",
          "label": "US 10Y yield"
        },
        {
          "series_id": "us_2y_yield",
          "label": "US 2Y yield"
        }
      ]
    },
    "references": []
  },
  "points_charged": 1.25,
  "remaining_points": 918.75,
  "request_id": "req_52a87c8e"
}
```

## 10.3 News Detail By ID

```http
GET /v1/news/{id}
```

Returns the same detail payload as `GET /v1/news/{type}/{slug}` by resolving the article ID returned from `GET /v1/news`.

This endpoint is a convenience path for clients that store FinNotes IDs. The primary human-readable path remains `GET /v1/news/{type}/{slug}`.

Required permission: `Basic(all news, report & data)`.

Point charge: based on the resolved article type.

| ID prefix | Resolved type | Point charge |
| --- | --- | ---: |
| `mn_` | `market-news` | 1.00 pt |
| `cn_` | `chart-news` | 1.25 pts |

If the ID resolves to a product without standalone detail pricing, the API returns `422 unsupported_detail_product` and deducts no points.

### Request

```bash
curl "https://api.finnotes.com/v1/news/cn_01jz9xd7nm?include_chart=true" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

The response envelope and payload fields are the same as the corresponding `GET /v1/news/{type}/{slug}` response.

## 10.4 Today Full Bundle

```http
GET /v1/news/today/full
```

Returns every published article from today (UTC date) with the same payload shape `/v1/news/{type}/{slug}` returns per article — `title`, `dek`, `body`, `references` (sources), and `chart` (chart-news only; `null` otherwise). One HTTP call replaces many detail calls for the common "give me today's full read" workflow.

Required permission: `Basic`.

### Pricing

Per-type detail-equivalent sum:

```text
total_charge = 1.00 × N(market-news)
             + 1.25 × N(chart-news)
```

Where N(type) is the number of articles of that type returned (after the type filter). Empty day (`N = 0` after filtering) charges `0.00 pt`.

Bundle pricing matches what fetching each item individually via `/v1/news/{type}/{slug}` would cost — this endpoint saves HTTP round-trips, not points.

### Query Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | string | `all` | Comma-separated kebab values: `market-news`, `chart-news`, or `all`. Restricts the bundle to one or more types. |

### Hard cap

If today contains more than **100** matching articles (after type filter), the endpoint returns `422 too_many_articles` and **does not charge points**. A normal newsroom day is well below this threshold; exceeding it indicates an upstream backfill or content-pipeline issue. Contact platform support if you see this in production.

### Caching

The bundled payload (article data + `meta`) is cached server-side for **5 minutes** keyed by `(date, type filter)`. Per-request fields (`request_id`, `points_charged`, `remaining_points`) are injected outside the cache. Articles published during the cache window become visible at the next refresh (≤ 5 min delay).

### Request

```bash
curl "https://api.finnotes.com/v1/news/today/full" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

```bash
# Restrict to a single type
curl "https://api.finnotes.com/v1/news/today/full?type=market-news" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

```json
{
  "request_id": "req_a1b2c3d4...",
  "points_charged": 2.25,
  "remaining_points": 1342.25,
  "data": [
    {
      "id": "mn_182",
      "type": "market-news",
      "slug": "cuba-approves-broad-economic-reforms-2026-06-19",
      "title": "Cuba approves broad economic reforms...",
      "url": "https://finnotes.com/market-news/cuba-approves-...",
      "dek": "Two-line teaser describing the article.",
      "body": "<full HTML body>",
      "category": "policy",
      "published_at": "2026-06-24T08:00:00Z",
      "importance_score": 9.1,
      "anomaly_score": 7.2,
      "references": [
        { "title": "Reuters report", "url": "https://reuters.com/..." }
      ],
      "chart": null
    },
    {
      "id": "cn_10",
      "type": "chart-news",
      "slug": "aluminum-reached-3654-on-may-1-2026-06-10",
      "title": "Aluminum reached 3,654 on May 1...",
      "url": "https://finnotes.com/charts/aluminum-reached-3654-...",
      "dek": "...",
      "body": "<full HTML body>",
      "category": "commodity",
      "published_at": "2026-06-24T06:30:00Z",
      "importance_score": 7.5,
      "anomaly_score": 6.8,
      "references": [],
      "chart": {
        "id": "chart_aluminum-spot",
        "title": "Aluminum spot price",
        "is_featured": true,
        "window_type": "dynamic",
        "dynamic_window_months": 12,
        "window_start_date": null,
        "window_end_date": null,
        "series": [
          { "series_id": "ALUMINUM", "label": "Aluminum (USD/t)" }
        ]
      }
    }
  ],
  "meta": {
    "date": "2026-06-24",
    "count": 2,
    "by_type": { "market-news": 1, "chart-news": 1 }
  }
}
```

`meta.by_type` only contains keys for the types requested via the `type` filter. `meta.count` equals `len(data)`.

### Errors

| Status | Code | When |
| --- | --- | --- |
| `400` | `invalid_type` | `type` contains a value other than the four allowed kebab strings. |
| `401` | `unauthorized` | Missing or invalid API key. |
| `402` | `insufficient_points` | Account does not have enough points for the total bundle charge. |
| `403` | `permission_denied` | Key lacks the `basic` permission. |
| `422` | `too_many_articles` | Today's matching count exceeds the 100-article cap (upstream issue). |
| `429` | `rate_limit_exceeded` | Account exceeded the per-second rate limit. |

## 10.6 Data-Series Lists

### Data Categories

```http
GET /v1/data-series/categories
```

Returns the available data categories. Clients can use the returned category `url` to request all data series in that category.

Required permission: `Basic(all news, report & data)`.

Point charge: first 10 data-series list requests per account per day are free; additional requests cost `5.00 pts` each.

Category directory items return only:

| Field | Description |
| --- | --- |
| `name` | Human-readable category name. |
| `slug` | Category slug used for filtering. |
| `url` | API URL for retrieving the data-series directory in this category. |

### Categories Request

```bash
curl "https://api.finnotes.com/v1/data-series/categories" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Categories Response

```json
{
  "data": [
    {
      "name": "Commodities",
      "slug": "commodities",
      "url": "https://api.finnotes.com/v1/data-series?category=commodities"
    },
    {
      "name": "Macro",
      "slug": "macro",
      "url": "https://api.finnotes.com/v1/data-series?category=macro"
    },
    {
      "name": "Rates",
      "slug": "rates",
      "url": "https://api.finnotes.com/v1/data-series?category=rates"
    }
  ],
  "daily_free_list_requests_remaining": 9,
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_categories_6f9c02"
}
```

### Data-Series Directory

```http
GET /v1/data-series
```

Returns a paginated directory of available data series. Clients can request the full directory or filter it by one category.

Required permission: `Basic(all news, report & data)`.

Point charge: first 10 data-series list requests per account per day are free; additional requests cost `5.00 pts` each.

This endpoint returns lightweight directory records only. It does not return frequency, source, date availability, latest values, or historical point values. Use `GET /v1/data-series/{slug}` when the client needs detailed metadata, and use `GET /v1/data-series/{slug}/points` when the client needs actual point values.

Data-series directory items return only:

| Field | Description |
| --- | --- |
| `name` | Human-readable series name. |
| `slug` | Series slug used in the detail endpoint. |
| `url` | API URL for retrieving the series detail payload. |
| `unit` | Unit for the series values, such as `USD/oz`, `%`, `index`, or `USD`. |

### Query Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `category` | string | None | Optional category slug returned by `GET /v1/data-series/categories`. If omitted, returns the full data-series directory. |
| `limit` | integer | `100` | Page size. Maximum `500`. |
| `cursor` | string | None | Cursor for the next page. |

The full directory may contain thousands of records. `GET /v1/data-series` is always paginated; clients must follow `pagination.next_cursor` to retrieve additional pages. Each cursor page is a separate data-series list request and counts toward the daily 10 free list requests. After the free allowance is exhausted, each cursor page costs 5.00 pts.

### All Series Request

```bash
curl "https://api.finnotes.com/v1/data-series?limit=100" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Category Series Request

```bash
curl "https://api.finnotes.com/v1/data-series?category=commodities&limit=100" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Series Directory Response

```json
{
  "data": [
    {
      "name": "Gold spot price",
      "slug": "gold",
      "url": "https://api.finnotes.com/v1/data-series/gold",
      "unit": "USD/oz"
    },
    {
      "name": "Annual GDP",
      "slug": "annual-gdp",
      "url": "https://api.finnotes.com/v1/data-series/annual-gdp",
      "unit": "USD"
    }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  },
  "daily_free_list_requests_remaining": 8,
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_catalog_13d904"
}
```

After the daily free data-series list allowance is exhausted, the same endpoint returns `points_charged: 5.0`:

```json
{
  "data": [
    {
      "name": "Gold spot price",
      "slug": "gold",
      "url": "https://api.finnotes.com/v1/data-series/gold",
      "unit": "USD/oz"
    }
  ],
  "pagination": {
    "next_cursor": "cur_next_page",
    "has_more": true
  },
  "daily_free_list_requests_remaining": 0,
  "points_charged": 5.0,
  "remaining_points": 917.0,
  "request_id": "req_catalog_paid_7aa2b1"
}
```

## 10.7 Data-Series Detail

```http
GET /v1/data-series/{slug}
```

Returns detailed metadata for one data series. This endpoint does not return historical data points, latest numeric value, or selected point values.

Required permission: `Basic(all news, report & data)`.

Point charge: `1.00 pt`.

### Path Parameters

| Parameter | Description |
| --- | --- |
| `slug` | Series slug returned by the data-series directory. |

### Request

```bash
curl "https://api.finnotes.com/v1/data-series/gold" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

```json
{
  "data": {
    "name": "Gold spot price",
    "slug": "gold",
    "url": "https://api.finnotes.com/v1/data-series/gold",
    "description": "Daily gold spot price in US dollars per troy ounce.",
    "category": {
      "name": "Commodities",
      "slug": "commodities",
      "url": "https://api.finnotes.com/v1/data-series?category=commodities"
    },
    "frequency": "daily",
    "unit": "USD/oz",
    "region": "GLOBAL",
    "source": "finnotes",
    "first_period_date": "1979-01-01",
    "latest_period_date": "2026-06-19",
    "default_order": "asc",
    "points_url": "https://api.finnotes.com/v1/data-series/gold/points"
  },
  "points_charged": 1.0,
  "remaining_points": 916.0,
  "request_id": "req_series_detail_4dc9f8"
}
```

## 10.8 Data-Series Points

```http
GET /v1/data-series/{slug}/points
```

Returns selected data points for one data series. This is the only data-series endpoint that returns point values.

Required permission: `Basic(all news, report & data)`.

Point charge:

| Result | Point charge |
| --- | ---: |
| Valid request returns `N` data points | `max(1, ceil(N / 250))` pts |
| Valid request, series exists, but the selected date window contains no data points | 1.00 pt |
| Validation error, unsupported range mode, missing required date, malformed date, `start_date > end_date`, permission error, or series not found | 0.00 pts |

Pricing is calculated per response page. If a selected range has more data than one page, each cursor request is charged by the number of data points returned in that response.

Examples:

| Returned data points in one response | Point charge |
| ---: | ---: |
| 0 valid empty window | 1.00 pt |
| 1-250 | 1.00 pt |
| 251-500 | 2.00 pts |
| 501-750 | 3.00 pts |

### Data Selection Modes

Exactly one data selection mode must be supplied.

| Mode | Required parameters | Meaning |
| --- | --- | --- |
| `range=all` | None | Select the full available history for the series. Use `limit` and `cursor` to page through large point sets. |
| `range=between` | `start_date`, `end_date` | Return points with `period_date >= start_date` and `period_date <= end_date`. |
| `range=since` | `start_date` | Return points from `start_date` through the latest available point. |

Invalid combinations:

| Invalid request | Reason | Point charge |
| --- | --- | ---: |
| `range=between&start_date=2024-01-01` | Missing `end_date`. | 0.00 pts |
| `range=since&end_date=2026-06-19` | `since` only accepts `start_date`. | 0.00 pts |
| `range=between&start_date=2026-01-01&end_date=2025-01-01` | `start_date` is after `end_date`. | 0.00 pts |
| `range=all&start_date=2024-01-01` | `all` does not accept date boundaries. | 0.00 pts |

Valid empty-window request:

```http
GET /v1/data-series/annual-gdp/points?range=between&start_date=2025-04-01&end_date=2025-05-01
```

If the series exists but has no points inside that valid window, the API returns `data.points: []`, `empty_window: true`, and `points_charged: 1.0`.

### Query Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `range` | string | Required | `all`, `between`, or `since`. |
| `start_date` | string | Conditional | Required for `between` and `since`. Date format: `YYYY-MM-DD`. |
| `end_date` | string | Conditional | Required for `between`; not accepted for `all` or `since`. Date format: `YYYY-MM-DD`. |
| `limit` | integer | `500` | Maximum point count. Maximum `5000`. |
| `cursor` | string | None | Cursor for the next page when the selected point set exceeds `limit`. |
| `order` | string | `asc` | `asc` or `desc`. |

### Since Request

```bash
curl "https://api.finnotes.com/v1/data-series/gold/points?range=since&start_date=2024-01-01&limit=500" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Since Response

```json
{
  "data": {
    "name": "Gold spot price",
    "slug": "gold",
    "range": {
      "mode": "since",
      "start_date": "2024-01-01",
      "end_date": "2026-06-19"
    },
    "empty_window": false,
    "returned_points": 2,
    "points": [
      {
        "period_date": "2026-06-18",
        "value": 2438.1
      },
      {
        "period_date": "2026-06-19",
        "value": 2450.25
      }
    ]
  },
  "pagination": {
    "next_cursor": null,
    "has_more": false
  },
  "points_charged": 1.0,
  "remaining_points": 915.0,
  "request_id": "req_843ea97d"
}
```

### All Points Request

```bash
curl "https://api.finnotes.com/v1/data-series/gold/points?range=all&limit=5000&order=asc" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Between Dates Request

```bash
curl "https://api.finnotes.com/v1/data-series/gold/points?range=between&start_date=2026-01-01&end_date=2026-06-19" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Empty Window Response

```json
{
  "data": {
    "name": "Annual GDP",
    "slug": "annual-gdp",
    "range": {
      "mode": "between",
      "start_date": "2025-04-01",
      "end_date": "2025-05-01"
    },
    "empty_window": true,
    "returned_points": 0,
    "points": []
  },
  "pagination": {
    "next_cursor": null,
    "has_more": false
  },
  "points_charged": 1.0,
  "remaining_points": 914.0,
  "request_id": "req_empty_5c137a"
}
```

## 10.9 Own Notes List

```http
GET /v1/notes
```

Returns user-owned Notes metadata for the authenticated account.

Required permission: `Notes Download & Upload`.

Point charge: first 10 Own Notes list requests per account per day are free; additional requests cost `1.00 pt` each. Each cursor page is a separate Own Notes list request and counts toward the daily 10 free Own Notes list requests.

### Query Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `limit` | integer | `50` | Page size. Maximum `100`. |
| `cursor` | string | None | Cursor for the next page. |
| `updated_after` | string | None | Return Notes updated after this UTC timestamp. |

### Request

```bash
curl "https://api.finnotes.com/v1/notes?limit=50" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

```json
{
  "data": [
    {
      "note_id": "note_01jz9z9x6m",
      "title": "Rates watchlist",
      "updated_at": "2026-06-19T07:10:00Z",
      "size_bytes": 18422
    }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  },
  "daily_free_notes_list_requests_remaining": 9,
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_436e9f31"
}
```

After the daily free Own Notes list allowance is exhausted, the same endpoint returns `points_charged: 1.0`.

## 10.10 Notes Download

```http
GET /v1/notes/{note_id}
```

Downloads one user-owned Note payload.

Required permission: `Notes Download & Upload`.

Point charge: `3.50 pts`.

### Request

```bash
curl "https://api.finnotes.com/v1/notes/note_01jz9z9x6m" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

```json
{
  "data": {
    "note_id": "note_01jz9z9x6m",
    "title": "Rates watchlist",
    "content_type": "text/markdown",
    "content": "# Rates watchlist\n\nNote content...",
    "updated_at": "2026-06-19T07:10:00Z"
  },
  "points_charged": 3.5,
  "remaining_points": 906.5,
  "request_id": "req_57ee9e2d"
}
```

## 10.11 Notes Import / Upload

```http
POST /v1/notes/import
```

Creates a new Note in the authenticated account workspace.

Required permission: `Notes Download & Upload`.

Point charge: `3.50 pts`.

**Quota policy.** API-imported Notes are governed by **point allowance only**, not by the workspace UI's note-count quota (Free 2 / Pro 25 / Max 100 — those caps apply only to in-browser Note creation). An API client can store many more Notes than the UI cap, paying 3.50 pts each. Maximum note body size is 500,000 characters; `title` and `external_id` are also bounded.

### Request Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | Yes | Note title. Max 200 chars. |
| `content_type` | string | Yes | Must be `text/markdown` in v1. Max 64 chars. |
| `content` | string | Yes | Markdown note body. Max 500,000 chars (~500 KB UTF-8). |
| `external_id` | string | Optional | Accepted for future-proofing. **Ignored in v1** — every call creates a fresh note. Max 200 chars. |

v1 limitations vs future versions:
- Only `text/markdown` is accepted. `application/json` is reserved for a later phase.
- `external_id` is parsed but does not trigger upsert in v1; the response `status` is always `created`. Clients that need upsert today should track the returned `note_id` themselves.

### Request

```bash
curl "https://api.finnotes.com/v1/notes/import" \
  -X POST \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Rates watchlist",
    "content_type": "text/markdown",
    "content": "# Rates watchlist\n\nNote content..."
  }'
```

### Response

```json
{
  "data": {
    "note_id": "note_42",
    "status": "created",
    "updated_at": "2026-06-19T12:00:00Z"
  },
  "points_charged": 3.5,
  "remaining_points": 903.0,
  "request_id": "req_4ee91d84"
}
```

## 10.12 Account Points

```http
GET /v1/account/points
```

Returns the authenticated account's current point state.

Required permission: `View Usage and remaining points`.

Point charge: `0.00 pts`.

### Request

```bash
curl "https://api.finnotes.com/v1/account/points" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

```json
{
  "data": {
    "plan": "pro",
    "monthly_allowance": 1350.0,
    "used_points": 428.0,
    "remaining_points": 922.0,
    "usage_rate": 0.317,
    "reset_at": "2026-06-30T00:00:00Z"
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_71a7bf80"
}
```

## 10.13 Monthly Usage

```http
GET /v1/usage
```

Returns the authenticated user's current-month usage as daily point totals.

This endpoint is intentionally privacy-limited. It does not return visited endpoints, URLs, slugs, article titles, data-series names, request IDs, API-key names, categories, user agents, IP addresses, or any request-level history. Request-level history is available only through the opt-in Log endpoints after logging has been enabled.

Required permission:

- `View Usage and remaining points` for the calling key's own daily point totals.
- `All API usage state` for account-wide daily point totals across all keys.

Point charge: `0.00 pts`.

### Query Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `range` | string | `this_month` | Must be `this_month` in `v1`. Other ranges are not exposed by this endpoint. |
| `scope` | string | `current_key` | `current_key` returns the calling key's usage. `account` returns account-wide totals and requires `All API usage state`. |

The response covers every calendar date in the current month. Past and current dates return decimal point totals, including `0.0` when no points were consumed. Future dates return `points: null` so clients can draw the full month while leaving future usage blank.

### Request

```bash
curl "https://api.finnotes.com/v1/usage?range=this_month&scope=account" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

```json
{
  "data": {
    "range": "this_month",
    "scope": "account",
    "month": "2026-06",
    "timezone": "UTC",
    "summary": {
      "used_points": 428.0,
      "remaining_points": 922.0,
      "monthly_allowance": 1350.0,
      "usage_rate": 0.317
    },
    "daily_points": [
      {
        "date": "2026-06-01",
        "points": 8.0
      },
      {
        "date": "2026-06-19",
        "points": 39.5
      },
      {
        "date": "2026-06-20",
        "points": null
      }
    ]
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_2b2df772"
}
```

## 10.14 Log Settings

```http
GET /v1/logs/settings
PATCH /v1/logs/settings
```

Request logs are opt-in. FinNotes does not collect user information for this log by default. When enabled, logs are visible only to the user who enabled them and to API keys with the required log permissions.

Required permissions:

- `View Log` for `GET /v1/logs/settings`.
- `Manage Log` for `PATCH /v1/logs/settings`.

Point charge: `0.00 pts`.

### Enable Logs

```bash
curl "https://api.finnotes.com/v1/logs/settings" \
  -X PATCH \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "retention_days": 30
  }'
```

### Response

```json
{
  "data": {
    "enabled": true,
    "retention_days": 30,
    "updated_at": "2026-06-19T12:00:00Z"
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_c14888df"
}
```

## 10.15 Request Logs

```http
GET /v1/logs
DELETE /v1/logs
```

Returns or deletes request logs after logging has been enabled.

Required permissions:

- `View Log` for `GET /v1/logs`.
- `Manage Log` for `DELETE /v1/logs`.

Point charge: `0.00 pts`.

> **Delete behavior:** `DELETE /v1/logs` hides previously stored request rows, but the delete action itself is recorded as a new visible row in subsequent `GET /v1/logs` responses. This is intentional — it gives the account-holder an audit trail that "logs were cleared at time X" so a deleted history can't be silently denied later.

### Query Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `range` | string | `last_24_hours` | `last_24_hours`, `last_7_days`, or `this_month`. |
| `status` | string | `all` | `all`, `2xx`, `4xx`, or `5xx`. |
| `api_key_id` | string | Optional | Filter to one API key. |
| `query` | string | Optional | Substring match against path and request_id. |
| `limit` | integer | `50` | Page size. Maximum `100`. |
| `cursor` | string | Optional | Cursor for next page. |

Query strings on the original request are not retained by the audit ledger and therefore the `query` field on each log entry is always `{}` in v1. Clients should rely on `path` for endpoint grouping.

### Request

```bash
curl "https://api.finnotes.com/v1/logs?range=last_24_hours&status=all" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Response

```json
{
  "data": [
    {
      "time": "2026-06-19T10:04:21Z",
      "request_id": "req_9f8a21c7",
      "api_key_name": "Production server",
      "method": "GET",
      "path": "/v1/news",
      "query": {},
      "status": 200,
      "points_charged": 1.0,
      "latency_ms": 184
    }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_aac59d4e"
}
```

## 10.16 Newsletter Preferences

> **Status: Phase 2 — not yet mounted in the API router.** Reading or writing this endpoint currently returns `404`. The contract is final; mounting is tracked separately. Do not integrate against this endpoint until announced. Newsletter preferences can still be managed via the developer console at `https://platform.finnotes.com/newsletter`.

```http
GET /v1/newsletter/preferences
PATCH /v1/newsletter/preferences
```

Manages free report subscriptions.

Required permission: `Manage Newsletter preferences`.

Point charge: `0.00 pts` for reading or saving preferences.

Free subscriptions:

| Subscription | Charge |
| --- | ---: |
| Daily Report | 0.00 pts |
| Weekly Report | 0.00 pts |
| Weekly Calendar | 0.00 pts |

### Read All Newsletter Subscription Status

`GET /v1/newsletter/preferences` returns the complete free Newsletter subscription state in one response. It does not query a single subscription.

```bash
curl "https://api.finnotes.com/v1/newsletter/preferences" \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Accept: application/json"
```

### Read Response

```json
{
  "data": {
    "subscriptions": {
      "daily_report": true,
      "weekly_report": false,
      "weekly_calendar": true
    },
    "updated_at": "2026-06-19T12:00:00Z"
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_newsletter_read_6c0d2c"
}
```

### Update One Or More Newsletter Subscriptions

`PATCH /v1/newsletter/preferences` accepts one or more subscription keys. Only submitted keys are changed; omitted subscriptions keep their existing state.

Valid subscription keys:

| Key | Subscription |
| --- | --- |
| `daily_report` | Daily Report |
| `weekly_report` | Weekly Report |
| `weekly_calendar` | Weekly Calendar |

### Update Request

```bash
curl "https://api.finnotes.com/v1/newsletter/preferences" \
  -X PATCH \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriptions": {
      "weekly_report": true,
      "weekly_calendar": false
    }
  }'
```

### Update Response

```json
{
  "data": {
    "subscriptions": {
      "daily_report": true,
      "weekly_report": true,
      "weekly_calendar": false
    },
    "updated_at": "2026-06-19T12:00:00Z"
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_a44e0500"
}
```

> ⏸️ **Sections 10.16, 10.17, and 11 are Phase 2 / Agent Skill scope and are not part of v1 launch.** The preference-save endpoints may be implemented earlier to let users configure intent, but the delivery side (webhook URL registration, HMAC signature spec, server-initiated retry, dead-letter handling) must be finalized before any API Push or Email push is actually delivered. Until then, setting `channels.api_push.enabled=true` returns `422 validation_error` with `code: "feature_unavailable"`.

## 10.17 Trace Data Release Push Preferences

```http
GET /v1/push/trace-data-release/preferences
PATCH /v1/push/trace-data-release/preferences
```

Configures immediate alerts for selected data releases and policy decisions.

Required permission: `Manage Newsletter preferences`.

Preference-save charge: `0.00 pts`.

Delivery charge: charged per triggered alert.

### Supported Release IDs

Initial supported release IDs:

| ID | Label |
| --- | --- |
| `us_cpi` | US CPI |
| `nonfarm_payrolls` | Nonfarm Payrolls |
| `fomc_rate_decision` | FOMC Rate Decision |
| `fed_chair_press_conference` | Fed Chair Press Conference |
| `ecb_rate_decision` | ECB Rate Decision |
| `eia_petroleum_status` | EIA Petroleum Status |
| `initial_jobless_claims` | Initial Jobless Claims |
| `us_gdp_advance` | US GDP Advance |

### Request Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | Yes | Turn this subscription on or off. |
| `release_ids` | array | Yes when enabled | Selected release and decision IDs. |
| `channels.email.enabled` | boolean | Yes | Enable email delivery. |
| `channels.email.use_default_email` | boolean | Required when email enabled | Use the account default email. |
| `channels.email.email` | string | Conditional | Required when email is enabled and default email is not used. |
| `channels.api_push.enabled` | boolean | Yes | Enable AI Agent API Push when available. |
| `channels.api_push.api_key_id` | string | Conditional | Active API key name or ID used by the mounted Agent Skill. |

API Push for Trace Data Release is reserved until the Agent Skill channel is enabled in production. If unavailable, setting `channels.api_push.enabled=true` returns `422 validation_error`.

### Request

```bash
curl "https://api.finnotes.com/v1/push/trace-data-release/preferences" \
  -X PATCH \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "release_ids": ["us_cpi", "nonfarm_payrolls", "fomc_rate_decision"],
    "channels": {
      "email": {
        "enabled": true,
        "use_default_email": true
      },
      "api_push": {
        "enabled": false,
        "api_key_id": null
      }
    }
  }'
```

### Response

```json
{
  "data": {
    "enabled": true,
    "release_ids": ["us_cpi", "nonfarm_payrolls", "fomc_rate_decision"],
    "channels": {
      "email": {
        "enabled": true,
        "use_default_email": true,
        "email": null
      },
      "api_push": {
        "enabled": false,
        "api_key_id": null
      }
    },
    "updated_at": "2026-06-19T12:00:00Z"
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_72f6cc3c"
}
```

## 10.18 Real-Time News Push Preferences

```http
GET /v1/push/news/preferences
PATCH /v1/push/news/preferences
```

Configures article push rules for newly published FinNotes articles.

Required permission: `Manage Newsletter preferences`.

Preference-save charge: `0.00 pts`.

Delivery charge: same as the active API retrieval price for each delivered article.

### Request Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | Yes | Turn this subscription on or off. |
| `types` | array | Yes when enabled | Any of `market-news`, `chart-news`. |
| `importance_score_min` | number | Yes | Minimum importance score, `0.0` to `10.0`. |
| `anomaly_score_min` | number | Yes | Minimum anomaly score, `0.0` to `10.0`. |
| `channels.email.enabled` | boolean | Yes | Enable email delivery. |
| `channels.email.use_default_email` | boolean | Required when email enabled | Use the account default email. |
| `channels.email.email` | string | Conditional | Required when email is enabled and default email is not used. |
| `channels.email.acknowledge_high_frequency_email` | boolean | Conditional | Must be `true` when enabling email for real-time news push. |
| `channels.api_push.enabled` | boolean | Yes | Send matching articles to a mounted AI Agent. |
| `channels.api_push.api_key_id` | string | Conditional | Active API key used by the mounted Agent Skill. |

Both `importance_score_min` and `anomaly_score_min` are applied. A delivered article must meet or exceed both floors.

### Request

```bash
curl "https://api.finnotes.com/v1/push/news/preferences" \
  -X PATCH \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "types": ["market-news", "chart-news"],
    "importance_score_min": 7.0,
    "anomaly_score_min": 7.0,
    "channels": {
      "email": {
        "enabled": true,
        "use_default_email": false,
        "email": "agent-managed-inbox@example.com",
        "acknowledge_high_frequency_email": true
      },
      "api_push": {
        "enabled": true,
        "api_key_id": "key_prod_001"
      }
    }
  }'
```

### Response

```json
{
  "data": {
    "enabled": true,
    "types": ["market-news", "chart-news"],
    "importance_score_min": 7.0,
    "anomaly_score_min": 7.0,
    "channels": {
      "email": {
        "enabled": true,
        "use_default_email": false,
        "email": "agent-managed-inbox@example.com"
      },
      "api_push": {
        "enabled": true,
        "api_key_id": "key_prod_001"
      }
    },
    "updated_at": "2026-06-19T12:00:00Z"
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_0d2ac970"
}
```

## 10.19 Notifications Preferences

> **Status: Phase 2 — not yet mounted in the API router.** Reading or writing this endpoint currently returns `404`. Notifications can still be configured via the developer console.

```http
GET /v1/notifications/preferences
PATCH /v1/notifications/preferences
```

Manages account email notification preferences for API platform events.

Required permission: `Notification management`.

Point charge: `0.00 pts`.

### Request

```bash
curl "https://api.finnotes.com/v1/notifications/preferences" \
  -X PATCH \
  -H "Authorization: Bearer $FINNOTES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "low_points_warning": true,
    "monthly_reset_notice": true,
    "key_status_change": true
  }'
```

### Response

```json
{
  "data": {
    "low_points_warning": true,
    "monthly_reset_notice": true,
    "key_status_change": true,
    "updated_at": "2026-06-19T12:00:00Z"
  },
  "points_charged": 0.0,
  "remaining_points": 922.0,
  "request_id": "req_3d2f3ad7"
}
```

## 11. AI Agent Push Delivery Payload

When API Push is enabled, FinNotes sends a signed JSON payload to the mounted Agent Skill channel.

### Common Event Envelope

```json
{
  "event_id": "evt_01jza0v9be",
  "event_type": "news.published",
  "created_at": "2026-06-19T12:00:00Z",
  "account_id": "acct_123",
  "api_key_id": "key_prod_001",
  "points_charged": 1.25,
  "remaining_points": 920.75,
  "data": {}
}
```

### News Push Event

```json
{
  "event_id": "evt_01jza0v9be",
  "event_type": "news.published",
  "created_at": "2026-06-19T12:00:00Z",
  "points_charged": 1.25,
  "remaining_points": 920.75,
  "data": {
    "id": "cn_01jz9xd7nm",
    "type": "chart-news",
    "title": "US yields reprice the policy path after payrolls",
    "url": "https://finnotes.com/charts/us-yields-reprice-policy-path",
    "dek": "A chart-backed read on the front-end rate reaction.",
    "importance_score": 8.1,
    "anomaly_score": 7.2
  }
}
```

### Trace Data Release Event

```json
{
  "event_id": "evt_01jza10h4n",
  "event_type": "trace.release_published",
  "created_at": "2026-06-19T12:30:00Z",
  "points_charged": 2.5,
  "remaining_points": 918.25,
  "data": {
    "release_id": "us_cpi",
    "release_name": "US CPI",
    "published_at": "2026-06-19T12:30:00Z",
    "headline": "US CPI prints below consensus",
    "url": "https://finnotes.com/market-news/us-cpi-below-consensus"
  }
}
```

## 12. Insufficient Points Behavior

If the account does not have enough points, the API returns `402 insufficient_points`.

No partial response is returned and no points are deducted.

```json
{
  "error": {
    "code": "insufficient_points",
    "message": "The account does not have enough points for this request.",
    "request_id": "req_730f988d",
    "details": {
      "required_points": 5.5,
      "remaining_points": 2.0
    }
  }
}
```

Recommended client behavior:

1. Stop retrying the same billable request.
2. Call `GET /v1/account/points` if the API key has permission.
3. Ask the user to upgrade, wait for monthly reset, or reduce the requested product scope.

## 13. Idempotency For Write Requests

Write endpoints should include `Idempotency-Key`.

Supported write endpoints include:

| Endpoint | Should use idempotency |
| --- | --- |
| `POST /v1/notes/import` | Yes |
| `PATCH /v1/newsletter/preferences` | Recommended |
| `PATCH /v1/push/trace-data-release/preferences` | Recommended |
| `PATCH /v1/push/news/preferences` | Recommended |
| `PATCH /v1/logs/settings` | Recommended |
| `DELETE /v1/logs` | Recommended |

If the same idempotency key is reused with the same request body, FinNotes returns the original result. If the same key is reused with a different body, FinNotes returns `409 idempotency_conflict`.

Idempotency keys are retained for **24 hours** from the first successful processing of the key. After 24 hours the key is forgotten and the same key + body combination will execute as a fresh request. Clients that need longer replay safety should rotate idempotency keys per logical operation.

## 14. Retry Guidance

Retry only transient errors:

| Status | Retry? | Guidance |
| ---: | --- | --- |
| `400`, `401`, `402`, `403`, `404`, `422` | No | Fix the request, key, permission, or account state. |
| `409` | No | Use a new idempotency key or resend the original body. |
| `429` | Yes | Wait for `Retry-After`. Rate-limit responses do not deduct points. |
| `500`, `503` | Yes | Retry with exponential backoff and jitter. |

Recommended backoff:

```text
1s, 2s, 4s, 8s, then stop or escalate
```

## 15. Python Example

```python
import os
import requests

api_key = os.environ["FINNOTES_API_KEY"]

response = requests.get(
    "https://api.finnotes.com/v1/news",
    params={"range": "today", "type": "all"},
    headers={
        "Authorization": f"Bearer {api_key}",
        "Accept": "application/json",
    },
    timeout=20,
)

if response.status_code == 402:
    raise RuntimeError("Insufficient FinNotes points")

response.raise_for_status()

payload = response.json()
print(payload["points_charged"], payload["remaining_points"])
print(payload["data"])
```

## 16. JavaScript Example

```js
const response = await fetch("https://api.finnotes.com/v1/news?range=today&type=all", {
  headers: {
    Authorization: `Bearer ${process.env.FINNOTES_API_KEY}`,
    Accept: "application/json",
  },
});

const payload = await response.json();

if (!response.ok) {
  throw new Error(payload.error?.message ?? "FinNotes API request failed");
}

console.log(payload.points_charged, payload.remaining_points);
console.log(payload.data);
```

## 17. Security Requirements

Production integrations must follow these rules:

1. Store API keys in a secret manager or server-side environment variable.
2. Never expose API keys in frontend browser code.
3. Use the minimum required key permissions.
4. Rotate keys after personnel changes, suspected leakage, or environment migration.
5. Use separate keys for production servers, automation workers, and AI Agent integrations.
6. Enable request logs only when needed and delete logs when they are no longer necessary.
7. Use dedicated agent-managed email inboxes for high-frequency real-time news push.

## 18. Versioning

The current version is `v1`.

Backward-compatible changes may be added to `v1`, including optional fields, new enum values, and new endpoints. Breaking changes require a new API version.

Clients should ignore unknown response fields.

## 19. Pricing Change Policy

Point pricing may change by product category. When point pricing changes, FinNotes should publish the effective date and preserve enough notice for production users to update usage budgets and automation rules.

Clients should not hard-code point prices as permanent constants. Use response headers and `GET /v1/account/points` for reconciliation.
