# Import API

> POST GeoJSON features or CSV and TSV rows into one type as drafts, with location rules, value conversion, matchBy updates, geocoding and result warnings.

The import endpoint loads GeoJSON features, or CSV/TSV rows that you send as JSON, into one type of a project. Every row becomes a **draft**. This page is for developers who load data by script; editors can use the import wizard in the web app, described on [Import data](https://spatibase.com/docs/content/import/).

Placeholders: `$PROJECT_ID` is the UUID in the project's address, and `jar.txt` holds the session cookie from [signing in](https://spatibase.com/docs/reference/management-api/#sign-in-from-a-script). The import is part of the [Content Management API](https://spatibase.com/docs/reference/management-api/), the web app's own interface, which can change without notice.

## At a glance

| | |
|---|---|
| Endpoint | `POST https://api.spatibase.com/api/projects/$PROJECT_ID/import` |
| Formats | a GeoJSON `FeatureCollection` or single `Feature`; CSV/TSV **parsed by you** into an array of row objects |
| Size | 10,000 rows per request (extra rows are ignored, with a warning; the MCP tool `import_features` takes 1,000); JSON body up to 50 MB |
| Location | GeoJSON geometry; CSV latitude and longitude columns, or an address column that Spatibase looks up with OpenStreetMap Nominatim (up to 250 different addresses per import) |
| Result | drafts; mark them ready and publish afterwards, or send `publish: true` to do both in the same request |
| Execution | synchronous: the request returns when the import is done, and all rows are written in one transaction |
| Who | project owners and editors |

Cells that don't fit an element's kind are left empty and reported as warnings; they do not fail the import.

## Request

| Field | Required | Meaning |
|---|---|---|
| `typeId` | yes | UUID of the type to import into. It must be switched on |
| `format` | yes | `geojson`, `csv` or `tsv` (`tsv` is treated exactly like `csv`) |
| `data` | yes | a GeoJSON object, or `[{column: cell, …}, …]` for CSV/TSV |
| `mapping` | no | which property or column feeds what (below) |
| `matchBy` | no | `apiKey` of an **identifier** element of the type: rows with a matching value update that entry |
| `groupIds` | no | groups that every created or updated entry is appended to |
| `locale` | no | a non-default [language](https://spatibase.com/docs/reference/import-api/#languages) of the project: the rows are translations of entries matched by `matchBy` |
| `publish` | no | `true` marks exactly the imported and updated entries ready and runs a project publish in the same request, as one new version. That publish also ships anything others had already marked ready. Entries that fail the publish checks stay drafts. Not with `locale`: publish translations afterwards. An assistant needs the Publish tier to send it |
| `dryRun` | no | `true` checks everything and returns the result without writing anything |

| `mapping` key | Meaning |
|---|---|
| `title` | Property or column for the title. Left out, the first non-empty one of `title`, `name`, `Title`, `Name`, `TITLE`, `NAME` is used. A missing title is allowed for drafts |
| `latitude`, `longitude` | CSV only: coordinate columns. A single comma is read as a decimal comma (`"41,3787"`) |
| `address` | CSV only: looked up when a row has no usable latitude and longitude |
| `elements` | `{elementApiKey: column}`. An element you don't list takes the column named exactly like its `apiKey`. To ignore such a column, map the element to `""` |

### Location rules

What happens to rows without a location depends on the type's `locationMode`:

| `locationMode` | Row without a location | CSV request without location columns |
|---|---|---|
| `required` | skipped, with a warning | `400` "Choose the latitude and longitude columns, or an address column, so entries can be placed on the map." |
| `optional` | created without a location, with a warning | imported without locations, with a warning |
| `none` | location ignored | location ignored |

A GeoJSON geometry must fit the type's `geometryKinds`, otherwise the row is skipped ("Trail entries can only be a line on the map"). A row that updates an existing entry through `matchBy` and has no location keeps the entry's current one.

## Example: CSV

Parse the file yourself, for example with Papa Parse and `header: true`, and send the rows:

```bash
curl -s -b jar.txt -X POST "https://api.spatibase.com/api/projects/$PROJECT_ID/import" \
  -H 'Content-Type: application/json' -d @import.json
```
```json
{
  "typeId": "b81bb7e3-f47a-4050-8afb-0d80cbffdda8",
  "format": "csv",
  "mapping": {
    "title": "name", "latitude": "lat", "longitude": "lng",
    "elements": { "code": "code", "sites": "sites", "dogs-allowed": "dogs", "difficulty": "difficulty",
                  "surface": "surface", "opens-at": "opens", "marker-color": "color", "regions": "region" }
  },
  "data": [
    { "name": "Gold Bluffs Beach", "code": "GBB", "lat": "41,3787", "lng": "-124.0897", "sites": "26", "dogs": "yes",
      "difficulty": "Easy", "surface": "Dirt; Gravel", "opens": "7:30 pm", "color": "#0a0", "region": "Redwood Coast" },
    { "name": "Elk Prairie", "code": "EP", "lat": "41.3579", "lng": "-124.0217", "sites": "seventy-five", "dogs": "maybe",
      "difficulty": "Expert", "opens": "9am", "color": "teal", "region": "Atlantis" },
    { "name": "Mill Creek", "code": "GBB", "sites": "145" }
  ]
}
```
```json
{
  "created": 3, "updated": 0, "skipped": 0,
  "warnings": [
    "1 row imported without a location: no location (latitude/longitude or address is missing) (row 3)",
    "Sites: 1 value couldn’t be used and was left empty (not a number) — row 2",
    "Dogs allowed: 1 value couldn’t be used and was left empty (not yes/no) — row 2",
    "Difficulty: 1 value couldn’t be used and was left empty (not one of the options) — row 2",
    "Marker color: 1 value couldn’t be used and was left empty (not a color) — row 2",
    "Regions: 1 value couldn’t be used and was left empty (no matching entry) — row 2",
    "Code: 1 value couldn’t be used and was left empty (Already used by “Gold Bluffs Beach”) — row 3"
  ]
}
```

The first row's draft, read back with `GET /entries/:entryId`:

```json
{ "title": "Gold Bluffs Beach", "status": "draft", "geometry": { "type": "Point", "coordinates": [-124.0897, 41.3787] },
  "values": { "code": "GBB", "sites": 26, "dogs-allowed": true, "difficulty": "easy", "surface": ["dirt", "gravel"],
              "opens-at": "19:30", "marker-color": "#00aa00", "regions": ["e52152c3-9c8c-44fa-b33b-e026ca9b42d5"] } }
```

Row 2 got `"opens-at": "09:00"`. Row 3 was created without a location and without a code, because `GBB` was already used.

## Value conversion per kind

Each cell is first converted to the element's kind, then checked with the same rules as a draft save ([values per kind](https://spatibase.com/docs/reference/management-api/#values-per-kind)). A value that fails either step is left empty and named in a warning. `required`, `minCount` and `minSelected` are not checked, because imported entries are drafts.

| `kind` | Accepted from a text cell | Also accepted (GeoJSON properties, JSON values) | Left empty, for example |
|---|---|---|---|
| `text`, `markdown` | any text; numbers and booleans become text | objects and arrays, only for `editor: json` (stored as JSON text) | objects in other text fields; invalid JSON for `editor: json`; too long |
| `identifier` | trimmed text | numbers | spaces or symbols; a value another entry already uses, including an earlier row of the same file |
| `number` | `26`, `1.6`, `12,5`, `1,234`, `1,234.5`, `1 234,5` | numbers | `seventy-five`; ambiguous commas such as `1,2345` or `1.234,5`; decimals for `integer`; outside `min` or `max` |
| `boolean` | `true yes y 1 on x` and `false no n 0 off`, any case | booleans | `maybe` |
| `date` (`date`) | ISO `2024-05-01`, or text a JavaScript `Date` can parse (`May 1, 2024` → `2024-05-01`) | | `2024-02-30` |
| `date` (`datetime`) | ISO text as is; other parseable text → a UTC ISO string | | unparseable text |
| `date` (`time`) | `9:30`, `09:30:15`, `7:30 pm` → `19:30`, `2pm` → `14:00` | | `25:00` |
| `date` (range) | not accepted from text (`May-Oct` is not parsed) | `{"start": …, "end": …}` | any text |
| `choice` | option value **or label**, any case, anywhere in the tree; `multiple` splits on `,` `;` `\|` | arrays | any unknown option (the whole value is dropped) |
| `link` | a URL → `{url}` | `{url, label}` | text without `http(s)://` |
| `embed` | a URL → `{url}` | `{url}` | a provider the element does not allow |
| `images`, `files` | asset ids separated by `,` `;` `\|` | arrays of ids | URLs, unknown ids, a non-image in `images` (the whole list is dropped) |
| `contact` | text becomes `{email}`, `{website}`, `{phone}` or `{name}`, by its shape | a contact object | an invalid email |
| `color` | `#0a0`, `0a0`, `#00AA00` → `#00aa00` | | `teal` |
| `reference` | an entry id, or the **title** of an entry of a target type (case-insensitive); `multiple` tries the whole text first, then splits on `,` `;` `\|` | arrays | no entry with that title |
| `geo` | GeoJSON written as text | GeoJSON objects | text that is not GeoJSON |
| `location` | not accepted from text (`-124.08,41.37` is not parsed) | `{"lng": …, "lat": …, "address": …}` | any text |
| `phone` | trimmed text or a number | | letters, fewer than 5 digits |

Things that surprise people:

- Numbers: a dot is always the decimal point. A comma followed by groups of exactly three digits is a thousands separator (`1,234` → `1234`). A single comma followed by one or two digits, with no dot, is a decimal comma (`12,5` → `12.5`). Any other comma is ambiguous, and the value is left empty with a warning rather than guessed. Spaces between digit groups are removed.
- References by title match only entries that existed **before** the request, so a row cannot reference an entry created by the same import. Import the targets first.
- Columns that match no element are ignored. The type's other elements stay empty.

## Update existing entries with matchBy

With `matchBy` set to the `apiKey` of an identifier element, a row whose value matches an entry of the import type, or of one of its subtypes, **updates that entry's draft**. Other rows are created as usual.

```bash
curl -s -b jar.txt -X POST "https://api.spatibase.com/api/projects/$PROJECT_ID/import" -H 'Content-Type: application/json' -d '{
  "typeId": "b81bb7e3-f47a-4050-8afb-0d80cbffdda8", "format": "csv", "matchBy": "code",
  "groupIds": ["7786b57d-26a2-47e7-b96d-c072aedc04b0"],
  "mapping": {"title":"name","latitude":"lat","longitude":"lng","elements":{"code":"code","sites":"sites"}},
  "data": [ {"code":"GBB","sites":"30"}, {"code":"GBB","sites":"31"},
            {"code":"NEW1","name":"Freshwater Lagoon","lat":"41.27","lng":"-124.09","sites":"12"} ] }'
```
```json
{ "created": 1, "updated": 1, "skipped": 1, "warnings": [ "1 row skipped: same Code as an earlier row in this file (row 2)" ] }
```

Gold Bluffs Beach kept its title, location and other values, got `"sites": 30`, and was added to the group.

- An update **merges** the imported values into the draft. This differs from `PATCH /entries/:entryId`, where `values` replaces everything. A row without a title or location keeps the entry's current one. The entry keeps its type.
- A published entry that is updated becomes `changed`. The Content Delivery API shows the change only after you mark it ready and publish.
- A second row matching the same entry is skipped. A row whose identifier belongs to an entry of an unrelated type is skipped too.
- `matchBy` that is not an identifier element of the type answers `422 errors.matchBy`.
- Without `matchBy`, running the same import twice creates duplicates.

## Languages

In a project with [languages](https://spatibase.com/docs/content/translations/) an import can fill in translations. There are two ways.

**Language columns.** Without `locale`, a column or GeoJSON property named `<field>_<code>`, `<field>:<code>` or `<field>.<code>` (for example `description_de`, `description:de` or `description.de`) is read as the German text of that field. For the title, use `title_de`, `name_de` or the name of your mapped title column plus the language. Case doesn't matter, and only the project's non-default languages count. The texts are saved as translations of the created or matched entries; an empty cell changes nothing. A dry run lists what it found as `languageColumns`:

```json
{ "created": 0, "updated": 2, "skipped": 0, "dryRun": true,
  "languageColumns": [ {"column": "name_de", "locale": "de", "field": "title"},
                       {"column": "description_de", "locale": "de", "field": "description"} ] }
```

**A translation file.** With `locale` and `matchBy`, every row updates only the translations of the entry it matches, and the row's title and text columns are the texts in that language:

```bash
curl -s -b jar.txt -X POST "https://api.spatibase.com/api/projects/$PROJECT_ID/import" -H 'Content-Type: application/json' -d '{
  "typeId": "b81bb7e3-f47a-4050-8afb-0d80cbffdda8", "format": "csv", "matchBy": "code", "locale": "de",
  "mapping": {"title": "name", "elements": {"code": "code", "description": "description"}},
  "data": [ {"code": "GBB", "name": "Goldbuckel-Strand", "description": "Ein langer Strand ..."} ] }'
```

- `locale` must be a non-default language of the project, otherwise `422 errors.locale`. It needs `matchBy` (`422 errors.matchBy`).
- A row that matches no entry is skipped with a warning, because there is nothing to translate. Nothing is created, and locations and other values stay as they are.
- Translations are drafts: a published entry becomes `changed` until you mark it ready and publish.
- Export writes the same information as `title:de` and `<apiKey>:de` properties, so an export can be edited and imported back.

## Example: GeoJSON

The feature geometry becomes the main location. Properties are matched like CSV columns, with the same automatic mapping by `apiKey`. Here the type `Trail` requires a line:

```json
{ "typeId": "74497dfa-e594-4897-8bc5-d46b3334755b", "format": "geojson", "matchBy": "slug",
  "mapping": { "elements": { "length-km": "km" } },
  "data": { "type": "FeatureCollection", "features": [
    { "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[-124.07, 41.21], [-124.06, 41.22]] },
      "properties": { "Name": "Tall Trees Grove", "slug": "tall-trees", "km": 6.4, "difficulty": "Moderate",
                      "open-season": { "start": "2026-06-01", "end": "2026-09-30" }, "trailhead": { "lng": -124.07, "lat": 41.21 } } },
    { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-124.0, 41.3] }, "properties": { "name": "Viewpoint", "slug": "viewpoint" } },
    { "type": "Feature", "geometry": null, "properties": { "name": "Somewhere", "slug": "somewhere" } },
    { "type": "Feature", "geometry": null, "properties": { "slug": "fern-canyon-loop", "km": "1,7" } } ] } }
```
```json
{ "created": 1, "updated": 1, "skipped": 2,
  "warnings": [ "1 row skipped: Trail entries can only be a line on the map (feature 2)", "1 row skipped: no geometry (feature 3)" ] }
```

Feature 1 was created with its range and location values from the JSON properties. Feature 4 had no geometry but matched an existing entry, so it updated `length-km` to `1.7` and kept the existing line.

## Address geocoding

When a CSV row has an address column but no usable latitude and longitude, Spatibase looks the address up with OpenStreetMap Nominatim.

```bash
curl -s -b jar.txt -X POST "https://api.spatibase.com/api/projects/$PROJECT_ID/import" -H 'Content-Type: application/json' -d '{
  "typeId":"b81bb7e3-f47a-4050-8afb-0d80cbffdda8","format":"csv","mapping":{"title":"name","address":"address"},
  "data":[{"name":"Crescent City visitor center","address":"1111 2nd St, Crescent City, CA 95531"},
          {"name":"Nowhere","address":"zzzz qqqq xxxx 99999 nonexistent"}]}'
```
```json
{ "created": 2, "updated": 0, "skipped": 0,
  "warnings": [ "1 row imported without a location: the address couldn’t be found on the map (row 2)" ] }
```

- Lookups run at about **one per second**, so 250 addresses take about 4 minutes, and the HTTP request stays open that long.
- Each different address is looked up once per import (case and spacing are ignored), so rows that share an address cost one lookup.
- Up to **250 different addresses per import**. Rows with further addresses are imported without a location, with a warning; in a type whose location is `required` they are skipped.
- A row with usable latitude and longitude is never looked up.
- Lookups share one queue with every other import running on Spatibase, so a busy moment can make yours slower. When the lookup service does not answer, the address counts as not found.
- A dry run does not look addresses up: they count as found, and a warning says how many would be looked up.

## Result and warnings

A successful import answers `200 {created, updated, ids, skipped, warnings: [string]}`, plus `dryRun: true` for a dry run.

- `ids` lists the id of the entry every row created or updated, in row order, with `null` for a row that was skipped. Use it to mark them ready, group or link the entries afterwards.
- With `publish: true` the answer also has `published` (how many went live), `publishFailed` (the entries that stayed drafts, with the reason) and `version` (the number of the version the publish created). They are marked ready and published in the same transaction as the import.
- A **longitude outside -180 to 180** is moved around the globe to fit and reported in a warning ("2 rows had a longitude outside -180 to 180 and were moved around the globe to fit: check that longitude and latitude aren’t swapped or broken"). Read it: swapped or broken coordinates still import.
- `skipped` counts rows that were not written at all. Values left empty do not count as skipped: the row is still created.
- Warnings are grouped by reason, so 10,000 bad rows produce a handful of lines. Each line names up to 5 rows (`row N` for CSV, `feature N` for GeoJSON, counted from 1 in `data`).
- Order: notes (such as "Only the first 10,000 of 10,001 rows were imported."), skipped rows, rows without a location, then values left empty per element.

| Request problem | Response |
|---|---|
| unknown or missing `typeId` | `422 errors.typeId` "Choose which type to import into." |
| type switched off | `422 errors.typeId` "Entries can’t be imported into “…” because the type is switched off." |
| `format` not `geojson`, `csv` or `tsv` | `400` `format must be "geojson" or "csv".` |
| GeoJSON `data` is not a FeatureCollection or Feature | `400` "The file isn’t a GeoJSON FeatureCollection or Feature." |
| CSV `data` is not an array (for example raw CSV text) | `400` "CSV data must be a list of rows." |
| `required` type, CSV without location columns | `400` (see [location rules](https://spatibase.com/docs/reference/import-api/#location-rules)) |
| invalid `matchBy`, `groupIds` or `dryRun` | `422 errors.matchBy`, `errors.groupIds` or `errors.dryRun` |

## Export drafts as GeoJSON

`GET /api/projects/$PROJECT_ID/export?type=$TYPE_ID` returns the **draft** of every entry of that type, whatever its status. Leave out `type` to export all types. For published content, use the Content Delivery API's `entries.geojson` ([GeoJSON and tiles](https://spatibase.com/docs/reference/geojson-and-tiles/)).

```bash
curl -s -b jar.txt "https://api.spatibase.com/api/projects/$PROJECT_ID/export?type=c9f0346b-3962-40ab-a8c2-d24d6e1597c3"
```
```json
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "id": "e52152c3-9c8c-44fa-b33b-e026ca9b42d5",
  "geometry": { "type": "Polygon", "coordinates": [[[-124.2, 41], [-123.8, 41], [-123.8, 41.5], [-124.2, 41.5], [-124.2, 41]]] },
  "properties": { "id": "e52152c3-9c8c-44fa-b33b-e026ca9b42d5", "type": "region", "title": "Redwood Coast", "status": "published" } } ] }
```

- `properties` holds `id`, `type` (the type's `apiKey`), `title`, `status` and the stored values: asset and entry **ids**, raw objects, and values of archived or unassigned elements too.
- `type` takes a type UUID and matches that type only, not its subtypes. `?type=region` answers `400 {"message":"Unknown type"}`.
- Entries without a location are included with `"geometry": null`. The response has `Content-Disposition: attachment`.
- Owners and editors can export. Exporting a type and importing the file back with `matchBy` updates the entries that have an identifier and creates the others again.

## Reproduce the web app wizard with the API

The import wizard in the web app parses the file in the browser and can create **a new type made from this file**. It guesses an element (field) per column, lets the editor adjust it, then calls the normal Content Management API. No server endpoint guesses fields, so a script does the same three steps itself.

### How the wizard guesses fields

Each column except the title and location columns is checked against every row, in this order:

| Guess | When | Element created |
|---|---|---|
| Structured data (JSON) | any value is an object or array (GeoJSON properties) | `text`, `editor: json` |
| Yes / No | all values are `true/false/yes/no/y/n/on/off` (at most 2 distinct); `1/0` only when the column name starts with `is`, `has`, `can`, `allow`, `enabled`, `active`, `open`, `available`, `visible` or `featured`, or ends in `flag` or `?` | `boolean` |
| ID / slug | the name is one of, or ends in `_`, `-` or a space followed by one of, `id`, `uuid`, `guid`, `slug`, `code`, `ref`, `key`, `identifier`, `sku` (`site_code`), and every row has a unique value of up to 64 letters, digits, `.`, `-`, `_` | `identifier` |
| Number | all numeric, none with a leading zero (ZIP codes stay text) | `number` (`integer: true` when all are whole) |
| Date, Date and time | all `YYYY-MM-DD`, or all ISO date-times | `date` with `mode: date` or `datetime` |
| Color | all `#rgb` or `#rrggbb` | `color` |
| Web link | all `http(s)://…` | `link` |
| Phone | the name contains `phone`, `mobile` or `fax` (or ends in `tel`), and the values look like phone numbers | `phone` |
| Choice | at least 6 values, 2 to 12 distinct (up to 20 with 100 or more values), values repeated, each up to 40 characters | `choice`; options are the distinct values, most common first |
| Multi-line text | any value over 200 characters or with a line break | `text`, `editor: multi` |
| Text | otherwise | `text`, `editor: single` |

Labels are tidied column names (`site_code` → `Site code`) and `apiKey`s are slugs (`site-code`). The type is named after the file (`campsites.csv` → `Campsites`).

Location: for GeoJSON, `geometryKinds` are the shape kinds present, and `locationMode` is `required` when every feature has a shape, `optional` when some do, `none` when none do. CSV latitude and longitude give `Point`, `required` only when every row has valid coordinates. An address column gives `optional`. No location gives `none`.

### The three calls

```bash
API=https://api.spatibase.com/api/projects/$PROJECT_ID
# 1. One element per column you keep (repeat for each)
curl -s -b jar.txt -X POST "$API/elements" -H 'Content-Type: application/json' \
  -d '{"label":"Category","apiKey":"category","kind":"choice","config":{"multiple":false,"options":[{"value":"tent","label":"Tent"},{"value":"rv","label":"RV"}]}}'
# → {"id":"14124f0e-…","apiKey":"category", …}

# 2. The type, with those elements in column order
curl -s -b jar.txt -X POST "$API/types" -H 'Content-Type: application/json' \
  -d '{"name":"Campsites","locationMode":"required","geometryKinds":["Point"],"elementIds":["45d8cb64-…","14124f0e-…"]}'
# → {"id":"401fcf4d-…","apiKey":"campsites", …}

# 3. The import, mapping each apiKey to its column
curl -s -b jar.txt -X POST "$API/import" -H 'Content-Type: application/json' -d '{
  "typeId":"401fcf4d-…","format":"csv",
  "mapping":{"title":"name","latitude":"lat","longitude":"lon","elements":{"site-code":"site_code","category":"category"}},
  "data":[{"name":"Mill Creek A1","lat":"41.701","lon":"-124.093","site_code":"MC-A1","category":"Tent"}]}'
# → {"created":1,"updated":0,"skipped":0,"warnings":[]}
```

The label `Tent` is matched to the option value `tent`. If creating an element returns `409` because the `apiKey` exists (an archived element keeps its key too), the wizard retries with `key-2`, `key-3` and `key-4`. If the type cannot be created, it deletes the elements it just made with `DELETE /elements/:elementId?permanent=true`.

## What is not supported

- **Other formats:** the API takes GeoJSON or parsed CSV/TSV rows, not raw CSV text or files. Excel, KML/KMZ, GPX, Shapefile and GeoParquet files, Google Sheets, Google My Maps and PostGIS tables are read by the [web app's import](https://spatibase.com/docs/content/import/), which sends the result here; from a script, convert them to GeoJSON first. GeoPackage cannot be imported.
- **Images and files from URLs:** `images` and `files` take ids of assets already uploaded. Upload first (`POST /assets`, or the `upload_asset` tool of the [MCP server](https://spatibase.com/docs/ai/mcp-server/) or the `--images` option of the [`spatibase` command](https://spatibase.com/docs/build/cli/)), then reference the ids.
- **Background imports:** there is no progress report and no import above 10,000 rows or 250 different addresses per request. Split large files.
- **CSV export:** export is GeoJSON only.
- **Translations of other fields:** only the title, `text` and `markdown` fields have language columns; choice option labels are set in the element, not by import.
- **Ranges and locations from text cells:** date ranges and `location` values need JSON objects, which only GeoJSON properties can carry.
- The web app wizard does not offer image, file, `geo` or `location` columns, although the API accepts asset ids and GeoJSON text for them.

## Related

- [The `spatibase` command](https://spatibase.com/docs/build/cli/): reads a file, imports it 1,000 rows at a time and publishes it
- [Content Management API](https://spatibase.com/docs/reference/management-api/)
- [Import data](https://spatibase.com/docs/content/import/)
- [Field values](https://spatibase.com/docs/reference/field-values/)
- [Limits](https://spatibase.com/docs/reference/limits/)
