Skip to content

Reading and editing features

Every synced table is served as an ArcGIS Feature Service. Existing GIS clients — the ArcGIS Maps SDK, ArcGIS Pro, QGIS’s Esri connector, anything that speaks f=json — point at the URL and work, with no translation layer. The same table is also available as plain GeoJSON and through the records API.

# one table = one single-layer service
https://{SERVER}/api/v0/rest/services/{baseId}/{tableName}/FeatureServer
# the whole base = one service, every table a layer
https://{SERVER}/api/v0/rest/services/{baseId}/FeatureServer

Append /0 for the first layer, then any Esri method. Both forms accept the same requests; the base-level form is what you want when tables reference each other, because relationships are only complete across a whole base.

Authenticate as you would any read: a session cookie, an app token, or an API key. Esri clients that only know ?token= work as-is — the token parameter takes an app token or an ims_… key.

Terminal window
curl 'https://{SERVER}/api/v0/rest/services/appXXX/Locations/FeatureServer/0/query' \
--get \
--data-urlencode "where=Status = 'Open' AND Capacity > 100" \
--data-urlencode 'outFields=*' \
--data-urlencode 'f=geojson' \
--data-urlencode 'token=ims_…'

The usual Esri query parameters are supported:

Parameter Notes
where SQL-92 subset — comparisons, AND/OR/NOT, IN, LIKE, BETWEEN, IS NULL
objectIds Comma-separated OBJECTIDs
geometry, geometryType, spatialRel, inSR Spatial filtering (envelope, point, polygon)
outFields * or a comma-separated list
returnGeometry, outSR Geometry on/off, reprojection
orderByFields Field ASC, Other DESC
resultOffset, resultRecordCount Paging
returnCountOnly, returnIdsOnly, returnExtentOnly, returnDistinctValues Cheap metadata queries
groupByFieldsForStatistics, outStatistics Server-side aggregation
f json (Esri), geojson, pbf where the client asks for it, html

Also available on a layer: /generateRenderer, /queryRelatedRecords, /getEstimates, /info, /layers, and /queryAttachments (see Attachments).

Source systems rarely have a stable integer id, so Sync Server derives the OBJECTID by hashing the provider’s record key. The hash is stable across syncs — a record keeps its OBJECTID as long as its key doesn’t change — but it is not a value to persist in another system as a long-term identifier. Use the provider record key (OID / Airtable recXXX / the NocoDB Id) for that; it’s exposed as a field and is what the records API and the activity log key on.

Terminal window
curl -X POST '…/FeatureServer/0/applyEdits' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'token=ims_…' \
--data-urlencode 'updates=[{"attributes":{"OBJECTID":1024,"Status":"Closed"}}]'

applyEdits takes adds, updates and deletes, and returns Esri’s addResults / updateResults / deleteResults. Writes go straight through to the upstream provider — an applyEdits against an Airtable-backed layer is an Airtable API call — and then update the cache in place, so the next read reflects the edit without waiting for a sync.

What you need to know before wiring up a client:

  • The provider must be writable. Airtable, NocoDB, Postgres, PostGIS and MSSQL are; fetch and tsip reject edits with a “not supported” error rather than pretending.
  • Write access is enforced per base — an org member role or better, an API key with write permission, or the base owner. A read key gets a 403.
  • Edits are serialized per table, so two clients editing the same table can’t interleave.
  • Every edit is logged, with a before-snapshot, and is the basis for rollback.
  • Every edit is broadcast over the realtime feed, so other clients on the same table update without polling.
  • Read-only fields stay read-only — formulas, rollups, lookups, autonumbers and created/modified stamps are rejected rather than silently dropped.

Each table carries a monotonic revision, reported on the realtime feed every time the table changes. Rollback accepts it as expectedRevision and returns 409 if someone else changed the table since you read it, rather than overwriting their work. Ordinary edits are not revision-gated — they are serialized by the write lock, and the realtime feed tells you when to re-read.

Feature services only have four usable scalar types, so everything else the provider knows about a column travels through Esri’s side channels — which means a well-behaved client gets the real types back:

Provider concept How it arrives
Single-select Coded-value domain
Multi-select JSON array value + description.multiValue
Number ranges Range domain
Currency, percent, rating, duration, URL, email, phone description.fieldValueType
Formula / rollup / lookup / autonumber editable: false
Created / modified time and by Layer-level editFieldsInfo

Clients that don’t recognise a key ignore it, so this is additive for existing consumers. Stored values are unchanged — a multi-select cell is the same JSON array literal it always was.

Link fields between tables in the same base are published as ArcGIS relationships, so queryRelatedRecords works and clients that understand related tables (the Maps SDK, Pro) show them. Relationships are built bidirectionally across the whole base, which is why they’re only complete on the base-level service URL:

GET /api/v0/rest/services/{baseId}/FeatureServer/relationships

One-to-one, one-to-many and many-to-many (through an intermediary table) are all supported.

If you just want the features:

GET /api/v0/{baseId}/{tableName}.geojson → the cached FeatureCollection
GET /api/v0/{baseId}/schema → the base's tables and their fields

Same auth rules, no Esri envelope. This is the cheapest way to feed MapLibre, deck.gl or a data-processing job.

The same table is also available as Mapbox Vector Tiles — see Vector tiles and snapshots.