Table sync API
Every Table sync action (7) and data model (2): endpoints, scopes, input, preconditions, events and examples.
Keep a table filled from an outside database, Airtable, a Google Sheet or a CSV link — or in step with your customers, both ways.
Module code data_sync · 7 actions · 2 data models · 8 events. A key or token never does more here than the person behind it can; row-level security applies exactly as in the app. Scopes: data_sync (everything), data_sync:read (read only) or data_sync:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
data_sync_runs | data_sync_run | /api/v1/data_sync_runs | |||
data_syncs | data_sync | /api/v1/data_syncs |
Lists take limit (up to 200), cursor, order (e.g. -created_at), fields and q; every other query parameter is a column filter: ?status=paid, ?total[gte]=100, ?id[in]=a,b, ?x=null.
Actions
| Action | Needs | What it does |
|---|---|---|
data_sync.create_sync | data_sync:manage | Sync a table from a source |
data_sync.delete_sync | data_sync:manage | Remove a sync |
data_sync.record_run | data_sync:handle | Record a sync run’s progress |
data_sync.request_run | data_sync:manage | Sync now |
data_sync.retry_run | data_sync:manage | Retry a failed run |
data_sync.set_paused | data_sync:manage | Pause or resume a sync |
data_sync.update_sync | data_sync:manage | Change a sync |
data_sync.create_sync
Sync a table from a source — Points a table at a source outside the platform and says how its fields map.
Needs data_sync:manage · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/data_syncs/actions/create_sync- Canonical address:
POST /api/v1/actions/data_sync.create_sync
Input
| Field | Type | Required | Notes |
|---|---|---|---|
dataTableId | uuid | yes | Table · id of data_table |
title | string | yes | Name · max 160 |
sourceKind | string (enum) | yes | Source · Allowed values: postgres, airtable, csv_url, module, google_sheets |
credentialId | uuid | Connection · id of credential | |
sourceConfig | object / array | yes | Where to read |
fieldMap | object / array | yes | Field mapping |
sourceKey | string | yes | Source key field · max 120 |
keyField | string | yes | Table key field · max 120 |
cursorField | string | Cursor field · max 120 | |
onSourceDelete | string (enum) | When a source record disappears · Allowed values: keep, delete | |
scheduleMinutes | number | Run every (minutes) · min 5 · max 10080 | |
twoWay | boolean | Edits in the table update the records |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
sync_table_not_found | A sync writes into a table. |
invalid_source_config | The source must say where to read, in its own terms. |
credential_required | A database or Airtable source reads with an Apps connection. |
key_field_unknown | Records are matched on a stored field of the table. |
invalid_field_map | Each source field maps onto one stored field. |
Events it publishes: data_sync.sync_created
What it writes: data_sync (insert) — Writes the sync
curl -X POST https://app.getera.io/api/v1/data_syncs/actions/create_sync \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"dataTableId":"DATA_TABLE_ID","title":"Name","sourceKind":"postgres","sourceConfig":{},"fieldMap":{},"sourceKey":"Source key field"}'{
"ok": true,
"result": "…",
"events": [
"data_sync.sync_created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.dataSyncs.createSync({
dataTableId: "DATA_TABLE_ID",
title: "Name",
sourceKind: "postgres",
sourceConfig: {},
fieldMap: {},
sourceKey: "Source key field"
})era data_syncs create_sync --data '{"dataTableId":"DATA_TABLE_ID","title":"Name","sourceKind":"postgres","sourceConfig":{},"fieldMap":{},"sourceKey":"Source key field"}'{
"name": "era__data_sync__create_sync",
"arguments": {
"dataTableId": "DATA_TABLE_ID",
"title": "Name",
"sourceKind": "postgres",
"sourceConfig": {},
"fieldMap": {},
"sourceKey": "Source key field"
}
}Errors: 401 no or expired credential · 403 the credential does not hold data_sync:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 sync_table_not_found, invalid_source_config, credential_required, key_field_unknown, invalid_field_map · 429 slow down
data_sync.delete_sync
Remove a sync — Stops syncing. The records it wrote stay in the table.
Needs data_sync:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/data_syncs/{id}/actions/delete_sync- Canonical address:
POST /api/v1/actions/data_sync.delete_sync
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Sync · id of data_sync |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
sync_not_found | The sync must exist. |
sync_running | A running sync is not removed from under itself. |
Events it publishes: data_sync.sync_deleted
What it writes: data_sync (delete) — Removes the sync and its run history
curl -X POST https://app.getera.io/api/v1/data_syncs/DATA_SYNC_ID/actions/delete_sync \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"data_sync.sync_deleted"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.dataSyncs.deleteSync("DATA_SYNC_ID")era data_syncs delete_sync DATA_SYNC_ID{
"name": "era__data_sync__delete_sync",
"arguments": {
"id": "DATA_SYNC_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold data_sync:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 sync_not_found, sync_running · 429 slow down
data_sync.record_run
Record a sync run’s progress — The sync job’s bookkeeping: counts, the checkpoint and the outcome.
Needs data_sync:handle · changes data
https://app.getera.io/api/v1/data_sync_runs/{id}/actions/record_run- Canonical address:
POST /api/v1/actions/data_sync.record_run
Input
| Field | Type | Required | Notes |
|---|---|---|---|
runId | uuid | yes | Filled from the path {id} · Run · id of data_sync_run |
status | string (enum) | yes | Status · Allowed values: running, succeeded, failed |
fetched | number | Read | |
inserted | number | Added | |
updated | number | Updated | |
deleted | number | Deleted | |
skipped | number | Skipped | |
cursorAfter | string | Checkpoint · max 2000 | |
error | string | Error · max 4000 | |
errors | object / array | Record errors |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
run_not_found | The run must exist. |
run_already_finished | A finished run is not written again. |
Events it publishes: data_sync.run_succeeded, data_sync.run_failed
What it writes: data_sync_run (update) — Adds the counts and the outcome; data_sync (update) — Moves the checkpoint and the health
curl -X POST https://app.getera.io/api/v1/data_sync_runs/DATA_SYNC_RUN_ID/actions/record_run \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"status":"running"}'{
"ok": true,
"result": "…",
"events": [
"data_sync.run_succeeded",
"data_sync.run_failed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.dataSyncRuns.recordRun("DATA_SYNC_RUN_ID", {
status: "running"
})era data_sync_runs record_run DATA_SYNC_RUN_ID \
--status "running"{
"name": "era__data_sync__record_run",
"arguments": {
"runId": "DATA_SYNC_RUN_ID",
"status": "running"
}
}Errors: 401 no or expired credential · 403 the credential does not hold data_sync:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 run_not_found, run_already_finished · 429 slow down
data_sync.request_run
Sync now — Queues a run as a platform job. A full run re-reads everything and can notice deletions.
Needs data_sync:manage · changes data
https://app.getera.io/api/v1/data_syncs/{id}/actions/request_run- Canonical address:
POST /api/v1/actions/data_sync.request_run
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Sync · id of data_sync |
fullRefresh | boolean | Re-read everything | |
trigger | string (enum) | Why · Allowed values: manual, schedule |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
sync_not_found | The sync must exist. |
sync_paused | A paused sync does not run. |
sync_running | One run at a time per sync. |
credential_required | The connection must still be there. |
Events it publishes: data_sync.run_requested
What it writes: data_sync_run (insert) — Queues the run; data_sync (update) — Marks the sync queued
curl -X POST https://app.getera.io/api/v1/data_syncs/DATA_SYNC_ID/actions/request_run \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"fullRefresh":true}'{
"ok": true,
"result": "…",
"events": [
"data_sync.run_requested"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.dataSyncs.requestRun("DATA_SYNC_ID", {
fullRefresh: true
})era data_syncs request_run DATA_SYNC_ID \
--full-refresh true{
"name": "era__data_sync__request_run",
"arguments": {
"id": "DATA_SYNC_ID",
"fullRefresh": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold data_sync:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 sync_not_found, sync_paused, sync_running, credential_required · 429 slow down
data_sync.retry_run
Retry a failed run — Queues a new run from the last committed checkpoint.
Needs data_sync:manage · changes data
https://app.getera.io/api/v1/data_sync_runs/{id}/actions/retry_run- Canonical address:
POST /api/v1/actions/data_sync.retry_run
Input
| Field | Type | Required | Notes |
|---|---|---|---|
runId | uuid | yes | Filled from the path {id} · Run · id of data_sync_run |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
run_not_found | The run must exist. |
run_not_failed | Only a failed run is retried. |
sync_not_found | The sync must still exist. |
sync_paused | A paused sync does not run. |
sync_running | One run at a time per sync. |
credential_required | The connection must still be there. |
Events it publishes: data_sync.run_requested
What it writes: data_sync_run (insert) — Queues the retry; data_sync (update) — Marks the sync queued
curl -X POST https://app.getera.io/api/v1/data_sync_runs/DATA_SYNC_RUN_ID/actions/retry_run \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"data_sync.run_requested"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.dataSyncRuns.retryRun("DATA_SYNC_RUN_ID")era data_sync_runs retry_run DATA_SYNC_RUN_ID{
"name": "era__data_sync__retry_run",
"arguments": {
"runId": "DATA_SYNC_RUN_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold data_sync:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 run_not_found, run_not_failed, sync_not_found, sync_paused, sync_running, credential_required · 429 slow down
data_sync.set_paused
Pause or resume a sync — A paused sync keeps its checkpoint and its schedule does not fire.
Needs data_sync:manage · changes data
https://app.getera.io/api/v1/data_syncs/{id}/actions/set_paused- Canonical address:
POST /api/v1/actions/data_sync.set_paused
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Sync · id of data_sync |
paused | boolean | yes | Paused |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
sync_not_found | The sync must exist. |
Events it publishes: data_sync.sync_paused, data_sync.sync_resumed
What it writes: data_sync (update) — Flips it between paused and idle
curl -X POST https://app.getera.io/api/v1/data_syncs/DATA_SYNC_ID/actions/set_paused \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"paused":true}'{
"ok": true,
"result": "…",
"events": [
"data_sync.sync_paused",
"data_sync.sync_resumed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.dataSyncs.setPaused("DATA_SYNC_ID", {
paused: true
})era data_syncs set_paused DATA_SYNC_ID \
--paused true{
"name": "era__data_sync__set_paused",
"arguments": {
"id": "DATA_SYNC_ID",
"paused": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold data_sync:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 sync_not_found · 429 slow down
data_sync.update_sync
Change a sync — Changes what it reads or how it maps. A changed key or source starts the next run from the beginning.
Needs data_sync:manage · changes data
https://app.getera.io/api/v1/data_syncs/{id}/actions/update_sync- Canonical address:
POST /api/v1/actions/data_sync.update_sync
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Sync · id of data_sync |
title | string | Name · max 160 | |
credentialId | uuid | Connection · id of credential | |
sourceConfig | object / array | Where to read | |
fieldMap | object / array | Field mapping | |
sourceKey | string | Source key field · max 120 | |
keyField | string | Table key field · max 120 | |
cursorField | string | Cursor field · max 120 | |
onSourceDelete | string (enum) | When a source record disappears · Allowed values: keep, delete | |
scheduleMinutes | number | Run every (minutes) · min 5 · max 10080 | |
twoWay | boolean | Edits in the table update the records |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
sync_not_found | The sync must exist. |
invalid_source_config | The source must say where to read, in its own terms. |
credential_required | A database or Airtable source reads with an Apps connection. |
key_field_unknown | Records are matched on a stored field of the table. |
invalid_field_map | Each source field maps onto one stored field. |
Events it publishes: data_sync.sync_changed
What it writes: data_sync (update) — Rewrites the sync
curl -X POST https://app.getera.io/api/v1/data_syncs/DATA_SYNC_ID/actions/update_sync \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Name"}'{
"ok": true,
"result": "…",
"events": [
"data_sync.sync_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.dataSyncs.updateSync("DATA_SYNC_ID", {
title: "Name"
})era data_syncs update_sync DATA_SYNC_ID \
--title "Name"{
"name": "era__data_sync__update_sync",
"arguments": {
"id": "DATA_SYNC_ID",
"title": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold data_sync:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 sync_not_found, invalid_source_config, credential_required, key_field_unknown, invalid_field_map · 429 slow down
Data models
DataSyncRun
Table data_sync_run · Resource data_sync_runs
GET /api/v1/data_sync_runsGET /api/v1/data_sync_runs/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
sync_id | string (uuid) · uuid | data_sync | ||
status | string · text | queued, running, succeeded, failed | ||
trigger | string · text | manual, schedule, retry | ||
retry_of | string (uuid) · uuid | yes | data_sync_run | |
cursor_before | string · text | yes | ||
cursor_after | string · text | yes | ||
fetched | integer · int4 | |||
inserted | integer · int4 | |||
updated | integer · int4 | |||
deleted | integer · int4 | |||
skipped | integer · int4 | |||
errors | object · jsonb | |||
error | string · text | yes | ||
started_at | string (date-time) · timestamptz | yes | ||
finished_at | string (date-time) · timestamptz | yes | ||
created_by | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((jsonb_typeof(errors) = 'array'::text))
DataSync
Table data_sync · Resource data_syncs
GET /api/v1/data_syncsGET /api/v1/data_syncs/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
data_table_id | string (uuid) · uuid | data_table | ||
title | string · text | |||
source_kind | string · text | postgres, airtable, csv_url, module, google_sheets | ||
source_config | object · jsonb | |||
field_map | object · jsonb | |||
source_key | string · text | |||
key_field | string · text | |||
cursor_field | string · text | yes | ||
cursor_value | string · text | yes | ||
on_source_delete | string · text | keep, delete | ||
schedule_minutes | integer · int4 | yes | ||
status | string · text | idle, queued, running, error, paused | ||
last_run_at | string (date-time) · timestamptz | yes | ||
last_success_at | string (date-time) · timestamptz | yes | ||
last_error | string · text | yes | ||
consecutive_failures | integer · int4 | |||
created_by | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
two_way | boolean · bool |
Primary key: id
Checks:
CHECK (((jsonb_typeof(source_config) = 'object'::text) AND (jsonb_typeof(field_map) = 'object'::text)))CHECK (((schedule_minutes IS NULL) OR ((schedule_minutes >= 5) AND (schedule_minutes <= 10080))))CHECK ((btrim(title) <> ''::text))CHECK (((NOT two_way) OR (source_kind = 'module'::text)))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
data_sync.run_failed | A sync run failed. | syncId, runId |
data_sync.run_requested | A sync run was queued. | syncId, runId, trigger |
data_sync.run_succeeded | A sync run finished. | syncId, runId |
data_sync.sync_changed | A sync’s source or mapping changed. | id |
data_sync.sync_created | A table was set to sync from a source. | id, dataTableId, sourceKind |
data_sync.sync_deleted | A sync was removed; its rows stayed. | id |
data_sync.sync_paused | A sync was paused. | id |
data_sync.sync_resumed | A sync was resumed. | id |