# Read and write

## Read the board

`GET /v1/ingest/board` · scope `read`

```bash
curl "https://neo-app-api-prod-836053680024.us-central1.run.app/v1/ingest/board?limit=100" \
  -H "Authorization: Bearer $SPECTER_KEY"
```

The answer carries the board's `columns`, a page of `rows` (newest first), `total`, and `write` —
exactly what a new row takes on this board.

| Query | Meaning |
| --- | --- |
| `limit` | Rows per page. Default 100, at most 1000. |
| `cursor` | The `nextCursor` from the previous page. Opaque. `nextCursor` is null on the last page. |
| `rows=false` | The shape and `write` only, no rows — the cheap way to learn what a row takes. Needs `read`. |

### What a row takes

`data.write.columns` lists every column: its `id` and `label` (either works as a cell key), its
`kind`, a `format` in words, an `example`, a select's `options`, a person column's `members`,
and `writable: false` with a `why` for columns the system fills. `data.write.example` is a whole
body that would land on this board — post it unchanged for a first successful write.

## Add a row

`POST /v1/ingest/rows` · scope `rows:write`

```bash
curl -X POST https://neo-app-api-prod-836053680024.us-central1.run.app/v1/ingest/rows \
  -H "Authorization: Bearer $SPECTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cells": {"Company": "Acme Ltd", "Amount": 25000, "Close date": "2026-10-01"}}'
```

A `201` answers the row in `data` and a receipt: `filled` (columns that now hold a value),
`coerced` (values changed on the way in — `"$1,200.50"` stored as `1200.5`) and `ignored` (cells
that stored nothing, and why).

## Add many rows

Send `rows` instead of `cells` — up to 500, **all or nothing**. A refusal names every bad row by
its position, so one fix-and-resend is enough:

```bash
curl -X POST https://neo-app-api-prod-836053680024.us-central1.run.app/v1/ingest/rows \
  -H "Authorization: Bearer $SPECTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rows": [{"cells": {"Company": "Acme Ltd"}}, {"cells": {"Company": "Globex", "Amount": 12000}}]}'
```

```json
{"error": {
  "message": "1 of 2 rows were refused and nothing was written — …",
  "errors": [{"index": 1, "message": "invalid value — Amount: not a number"}]
}}
```

## Cell formats

Strings always work; numbers and booleans may be JSON values. Each column's `format` is the
authority for that board.

| Kind | Send | Example |
| --- | --- | --- |
| text, long_text | text; newlines kept | `"Acme Ltd"` |
| number, percent | a decimal | `1250.5` |
| currency | an amount; written amounts are read | `25000` or `"$25,000"` |
| rating | a whole number, 1 to the column's max | `4` |
| date | `YYYY-MM-DD` | `"2026-09-09"` |
| datetime | RFC 3339, stored in UTC | `"2026-09-09T14:05:00Z"` |
| select | one of `options` | `"new"` |
| checkbox | a boolean | `true` |
| url, email, phone | the text | `"owner@acme.example"` |
| person | a member's id or email, from `members` | `"ana@yourfirm.example"` |
| tags | an array of labels, or one | `["referral", "priority"]` |
| address | an object; every key optional | `{"line1": "1 Main St", "city": "Austin"}` |
| relation | `{"id", "label"}` of a record on the related page | |
| formula, lookup, file | not writable on a new row | |

## Retrying safely

On a page key (Leads, Deals…), send `external_id` — your id for the record, up to 255 bytes. Posting
the same one again answers the first record instead of making a second, so a timed-out request can
be retried. Board keys refuse `external_id`; read the board before re-sending instead.
