Era insights API
Every Era insights action (6) and data model (2): endpoints, scopes, input, preconditions, events and examples.
Era tells you before you ask: what changed in your work, with sources, caveats and the next step — yours only.
Module code insights · 6 actions · 2 data models · 0 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: insights (everything), insights:read (read only) or insights:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
insight_settings | member_insight_setting | /api/v1/insight_settings | |||
insights | member_insight | /api/v1/insights |
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 |
|---|---|---|
insights.ack | insights:write | Mark an insight |
insights.feedback | insights:write | Was this useful? |
insights.mark_delivered | insights:write | Mark an insight delivered |
insights.mute | insights:write | Mute this kind |
insights.record | insights:write | Note an insight for me |
insights.update_settings | insights:write | Insight settings |
insights.ack
Mark an insight — Seen, done or dismissed (or expired, when the situation resolved).
Needs insights:write · changes data
https://app.getera.io/api/v1/insights/{id}/actions/ack- Canonical address:
POST /api/v1/actions/insights.ack
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Insight · id of member_insight |
status | string (enum) | Status · Allowed values: seen, done, dismissed, expired |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
bad_status | The status is seen, done, dismissed or expired. |
insight_not_found | The insight must be one of yours. |
What it writes: member_insight (update) — Saves the status
curl -X POST https://app.getera.io/api/v1/insights/INSIGHT_ID/actions/ack \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"status":"seen"}'{
"ok": true,
"result": "…",
"events": []
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.insights.ack("INSIGHT_ID", {
status: "seen"
})era insights ack INSIGHT_ID \
--status "seen"{
"name": "era__insights__ack",
"arguments": {
"id": "INSIGHT_ID",
"status": "seen"
}
}Errors: 401 no or expired credential · 403 the credential does not hold insights:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 bad_status, insight_not_found · 429 slow down
insights.feedback
Was this useful? — Useful or not useful — Era ranks that kind higher or lower for you.
Needs insights:write · changes data
https://app.getera.io/api/v1/insights/{id}/actions/feedback- Canonical address:
POST /api/v1/actions/insights.feedback
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Insight · id of member_insight |
verdict | string (enum) | yes | Verdict · Allowed values: useful, not_useful |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
bad_verdict | The verdict is useful or not useful. |
insight_not_found | The insight must be one of yours. |
What it writes: member_insight (update) — Saves your verdict; member_insight_setting (update) — Moves that kind’s rank for you
curl -X POST https://app.getera.io/api/v1/insights/INSIGHT_ID/actions/feedback \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"verdict":"useful"}'{
"ok": true,
"result": "…",
"events": []
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.insights.feedback("INSIGHT_ID", {
verdict: "useful"
})era insights feedback INSIGHT_ID \
--verdict "useful"{
"name": "era__insights__feedback",
"arguments": {
"id": "INSIGHT_ID",
"verdict": "useful"
}
}Errors: 401 no or expired credential · 403 the credential does not hold insights:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 bad_verdict, insight_not_found · 429 slow down
insights.mark_delivered
Mark an insight delivered — A surface (the bell, the desktop chat, a desktop notification) says it showed the insight.
Needs insights:write · changes data
https://app.getera.io/api/v1/insights/{id}/actions/mark_delivered- Canonical address:
POST /api/v1/actions/insights.mark_delivered
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Insight · id of member_insight |
channel | string (enum) | yes | Channel · Allowed values: desktop_chat, desktop_notification, bell, digest_email, digest_whatsapp |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
bad_channel | The channel is one insights go to. |
insight_not_found | The insight must be one of yours. |
What it writes: member_insight (update) — Stamps where it was shown
curl -X POST https://app.getera.io/api/v1/insights/INSIGHT_ID/actions/mark_delivered \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"channel":"desktop_chat"}'{
"ok": true,
"result": "…",
"events": []
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.insights.markDelivered("INSIGHT_ID", {
channel: "desktop_chat"
})era insights mark_delivered INSIGHT_ID \
--channel "desktop_chat"{
"name": "era__insights__mark_delivered",
"arguments": {
"id": "INSIGHT_ID",
"channel": "desktop_chat"
}
}Errors: 401 no or expired credential · 403 the credential does not hold insights:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 bad_channel, insight_not_found · 429 slow down
insights.mute
Mute this kind — Stop (or resume) one kind of insight for you.
Needs insights:write · changes data
https://app.getera.io/api/v1/insight_settings/actions/mute- Canonical address:
POST /api/v1/actions/insights.mute
Input
| Field | Type | Required | Notes |
|---|---|---|---|
kind | string (enum) | yes | Kind · Allowed values: assigned_to_me, dependency_changed, blocker, deadline, customer_complaint, order_late, invoice_overdue, conflicting_facts, asked_of_me, today_open_questions |
muted | boolean | Muted · false resumes it |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
unknown_kind | The kind is one Era knows. |
What it writes: member_insight_setting (update) — Switches the kind off or on for you; member_insight (update) — Dismisses what was waiting of that kind
curl -X POST https://app.getera.io/api/v1/insight_settings/actions/mute \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"kind":"assigned_to_me"}'{
"ok": true,
"result": "…",
"events": []
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.insightSettings.mute({
kind: "assigned_to_me"
})era insight_settings mute \
--kind "assigned_to_me"{
"name": "era__insights__mute",
"arguments": {
"kind": "assigned_to_me"
}
}Errors: 401 no or expired credential · 403 the credential does not hold insights:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 unknown_kind · 429 slow down
insights.record
Note an insight for me — Era’s sweep notes one thing that matters to you, with its sources, what it could not verify and the next step.
Needs insights:write · changes data
https://app.getera.io/api/v1/insights/actions/record- Canonical address:
POST /api/v1/actions/insights.record
Input
| Field | Type | Required | Notes |
|---|---|---|---|
kind | string (enum) | yes | Kind · Allowed values: assigned_to_me, dependency_changed, blocker, deadline, customer_complaint, order_late, invoice_overdue, conflicting_facts, asked_of_me, today_open_questions |
dedupeKey | string | yes | Same-situation key · max 300 |
title | string | yes | Title · max 200 |
body | string | Message · max 4000 | |
severity | string (enum) | How pressing · Allowed values: info, attention, urgent | |
score | number | Rank · min -10 · max 10 | |
facts | object / array | Facts it was drawn from | |
sources | object / array | Sources · [{label, href, kind, ref}] | |
unverified | object / array | What could not be verified | |
actions | object / array | Next steps · [{kind: open|reply|create_task|ask, label, href}] | |
locale | string (enum) | Language · Allowed values: tr, en | |
composedBy | object / array | Written by | |
deliverAfter | date-time | Hold until | |
expiresAt | date-time | Expires |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
unknown_kind | An insight is one of the kinds Era knows. |
empty_title | An insight needs a title. |
invalid_evidence | Sources, facts, caveats and next steps are short lists of the right shape. |
already_noted | The same situation in the same state is told once. |
What it writes: member_insight (insert) — Adds the insight, for you only
curl -X POST https://app.getera.io/api/v1/insights/actions/record \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"kind":"assigned_to_me","dedupeKey":"Same-situation key","title":"Title"}'{
"ok": true,
"result": "…",
"events": []
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.insights.record({
kind: "assigned_to_me",
dedupeKey: "Same-situation key",
title: "Title"
})era insights record \
--kind "assigned_to_me" \
--dedupe-key "Same-situation key" \
--title "Title"{
"name": "era__insights__record",
"arguments": {
"kind": "assigned_to_me",
"dedupeKey": "Same-situation key",
"title": "Title"
}
}Errors: 401 no or expired credential · 403 the credential does not hold insights:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 unknown_kind, empty_title, invalid_evidence, already_noted · 429 slow down
insights.update_settings
Insight settings — Turn Era’s insights on or off, choose kinds, quiet hours, channels and a daily limit.
Needs insights:write · changes data
https://app.getera.io/api/v1/insight_settings/actions/update_settings- Canonical address:
POST /api/v1/actions/insights.update_settings
Input
| Field | Type | Required | Notes |
|---|---|---|---|
enabled | boolean | On | |
kinds | object / array | Kinds · {kind: true|false} | |
quietStart | number | Quiet from (hour) · min 0 · max 23 | |
quietEnd | number | Quiet until (hour) · min 0 · max 23 | |
timezone | string | Time zone · max 64 | |
channels | object / array | Channels · desktop_chat · desktop_notification · bell · digest_email · digest_whatsapp | |
dailyCap | number | At most per day · min 0 · max 20 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
bad_hours | Quiet hours are whole hours in a known time zone. |
bad_cap | The daily limit is 0 to 20. |
bad_switches | Kinds and channels are known names with on/off values. |
What it writes: member_insight_setting (update) — Saves your insight settings
curl -X POST https://app.getera.io/api/v1/insight_settings/actions/update_settings \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"enabled":true}'{
"ok": true,
"result": "…",
"events": []
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.insightSettings.updateSettings({
enabled: true
})era insight_settings update_settings \
--enabled true{
"name": "era__insights__update_settings",
"arguments": {
"enabled": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold insights:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 bad_hours, bad_cap, bad_switches · 429 slow down
Data models
MemberInsightSetting
Table member_insight_setting · Resource insight_settings
GET /api/v1/insight_settingsGET /api/v1/insight_settings/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
user_id | string (uuid) · uuid | app_user | ||
enabled | boolean · bool | |||
kinds | object · jsonb | |||
weights | object · jsonb | |||
quiet_start | integer · int2 | |||
quiet_end | integer · int2 | |||
timezone | string · text | |||
channels | object · jsonb | |||
daily_cap | integer · int2 | |||
watermark | string (int64) · int8 | |||
last_swept_at | string (date-time) · timestamptz | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Unique: workspace_id, user_id
Checks:
CHECK ((jsonb_typeof(channels) = 'object'::text))CHECK (((daily_cap >= 0) AND (daily_cap <= 20)))CHECK ((jsonb_typeof(kinds) = 'object'::text))CHECK (((quiet_end >= 0) AND (quiet_end <= 23)))CHECK (((quiet_start >= 0) AND (quiet_start <= 23)))CHECK (((length(timezone) >= 1) AND (length(timezone) <= 64)))CHECK ((jsonb_typeof(weights) = 'object'::text))
MemberInsight
Table member_insight · Resource insights
GET /api/v1/insightsGET /api/v1/insights/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
user_id | string (uuid) · uuid | app_user | ||
kind | string · text | assigned_to_me, dependency_changed, blocker, deadline, customer_complaint, order_late, invoice_overdue, conflicting_facts, asked_of_me, today_open_questions | ||
dedupe_key | string · text | |||
severity | string · text | info, attention, urgent | ||
score | number · float4 | |||
title | string · text | |||
body | string · text | |||
facts | object · jsonb | |||
sources | object · jsonb | |||
unverified | object · jsonb | |||
actions | object · jsonb | |||
locale | string · text | tr, en | ||
composed_by | object · jsonb | |||
status | string · text | new, seen, done, dismissed, expired | ||
feedback | string · text | yes | ||
delivered | object · jsonb | |||
deliver_after | string (date-time) · timestamptz | yes | ||
expires_at | string (date-time) · timestamptz | yes | ||
seen_at | string (date-time) · timestamptz | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Unique: workspace_id, user_id, dedupe_key
Checks:
CHECK ((jsonb_typeof(actions) = 'array'::text))CHECK ((length(body) <= 4000))CHECK ((jsonb_typeof(composed_by) = 'object'::text))CHECK (((length(dedupe_key) >= 1) AND (length(dedupe_key) <= 300)))CHECK ((jsonb_typeof(delivered) = 'object'::text))CHECK ((jsonb_typeof(facts) = 'array'::text))CHECK (((feedback IS NULL) OR (feedback = ANY (ARRAY['useful'::text, 'not_useful'::text]))))CHECK ((jsonb_typeof(sources) = 'array'::text))CHECK (((length(btrim(title)) >= 1) AND (length(btrim(title)) <= 200)))CHECK ((jsonb_typeof(unverified) = 'array'::text))