Sales API
Every Sales action (31) and data model (11): endpoints, scopes, input, preconditions, events and examples.
Leads, the calls that follow them up, and the partners who send them.
Module code crm · 31 actions · 11 data models · 27 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: crm (everything), crm:read (read only) or crm:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
affiliate_report_links | affiliate_report_link | /api/v1/affiliate_report_links | |||
affiliates | affiliate | /api/v1/affiliates | crm.save_affiliate | ||
call_results | call_result | /api/v1/call_results | |||
call_statuses | call_status | /api/v1/call_statuses | |||
call_templates | call_template | /api/v1/call_templates | |||
calls | call | /api/v1/calls | |||
lead_cancellations | lead_cancellation | /api/v1/lead_cancellations | |||
lead_sources | lead_source | /api/v1/lead_sources | |||
lead_stages | lead_stage | /api/v1/lead_stages | |||
leads | lead | /api/v1/leads | crm.capture_lead | crm.update_lead | crm.archive_lead |
lost_reasons | lost_reason | /api/v1/lost_reasons |
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 |
|---|---|---|
crm.amend_call | crm:write | Correct a call |
crm.archive_lead | crm:delete | Archive a lead |
crm.assign_lead | crm:write | Assign a lead |
crm.attribute_lead | crm:write | Credit a lead to an affiliate |
crm.cancel_lead | crm:write | Cancel a lead |
crm.capture_lead | crm:write | Open a lead |
crm.create_call_template | crm:manage | Save a call template |
crm.deactivate_affiliate | crm:manage | Close an affiliate |
crm.deactivate_call_template | crm:manage | Close a call template |
crm.deactivate_lost_reason | crm:manage | Close a loss reason |
crm.export_calls | crm:export | Export calls to Excel |
crm.export_leads | crm:export | Export leads |
crm.issue_affiliate_report_link | crm:manage | Send an affiliate their report link |
crm.log_call | crm:write | Log a call |
crm.merge_lost_reasons | crm:manage | Merge loss reasons |
crm.raise_stale_leads | crm:write | Flag quiet leads |
crm.restore_lead | crm:delete | Restore a lead |
crm.revoke_affiliate_report_link | crm:manage | Close an affiliate report link |
crm.save_affiliate | crm:manage | Add or edit an affiliate |
crm.score_leads | crm:write | Score leads |
crm.set_call_option_active | crm:manage | Open or close a call option |
crm.set_call_result | crm:manage | Add or change a call result |
crm.set_call_status | crm:manage | Add or change a call outcome |
crm.set_lead_source | crm:manage | Add or rename a lead source |
crm.set_lead_source_active | crm:manage | Open or close a lead source |
crm.set_lead_stage | crm:write | Move a lead |
crm.set_lead_stage_active | crm:manage | Open or close a stage |
crm.set_lead_stage_definition | crm:manage | Add or change a stage |
crm.set_lost_reason | crm:manage | Add or rename a loss reason |
crm.start_trial | crm:write | Start a trial |
crm.update_lead | crm:write | Edit a lead |
crm.amend_call
Correct a call — Fixes a logged call — and re-runs the auto-close rule, as the source does.
Needs crm:write · changes data
https://app.getera.io/api/v1/calls/{id}/actions/amend_call- Canonical address:
POST /api/v1/actions/crm.amend_call
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Call · id of call |
statusId | uuid | Outcome · id of call_status | |
resultId | uuid | Result · id of call_result | |
notes | string | Notes · max 8000 | |
durationMinutes | number | Duration (minutes) · min 0 · max 1440 | |
isImportant | boolean | Important | |
followUpDate | date | Follow up on | |
followUpTime | string | At · max 8 | |
followUpNotes | string | Follow-up notes · max 4000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
call_not_found | The call must exist. |
lead_not_found | Its lead must exist. |
call_status_not_available | The outcome must exist and be active. |
call_result_not_available | The result must exist and be active. |
follow_up_in_the_past | A moved follow-up must not land in the past. |
Events it publishes: crm.call_amended, crm.lead_stage_changed, crm.lead_lost
What it writes: call (update) — Corrects the call; lead (update) — Re-evaluates the auto-close rule
curl -X POST https://app.getera.io/api/v1/calls/CALL_ID/actions/amend_call \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"statusId":"CALL_STATUS_ID"}'{
"ok": true,
"result": "…",
"events": [
"crm.call_amended",
"crm.lead_stage_changed",
"crm.lead_lost"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.calls.amendCall("CALL_ID", {
statusId: "CALL_STATUS_ID"
})era calls amend_call CALL_ID \
--status-id "CALL_STATUS_ID"{
"name": "era__crm__amend_call",
"arguments": {
"id": "CALL_ID",
"statusId": "CALL_STATUS_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 call_not_found, lead_not_found, call_status_not_available, call_result_not_available, follow_up_in_the_past · 429 slow down
crm.archive_lead
Archive a lead — Takes the lead out of lists without deleting it or its calls.
Needs crm:delete · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/leads/{id}/actions/archive_lead- REST alias:
DELETE /api/v1/leads/{id} - Canonical address:
POST /api/v1/actions/crm.archive_lead
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Lead · id of lead |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
lead_not_found | The lead must exist. |
already_archived | The lead must still be active. |
Events it publishes: crm.lead_archived
What it writes: lead (update) — Stamps archived_at
curl -X POST https://app.getera.io/api/v1/leads/LEAD_ID/actions/archive_lead \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_archived"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.archiveLead("LEAD_ID")era leads archive_lead LEAD_ID{
"name": "era__crm__archive_lead",
"arguments": {
"id": "LEAD_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:delete · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 lead_not_found, already_archived · 429 slow down
crm.assign_lead
Assign a lead — Gives the lead an owner, or returns it to the pool.
Needs crm:write · changes data
https://app.getera.io/api/v1/leads/{id}/actions/assign_lead- Canonical address:
POST /api/v1/actions/crm.assign_lead
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Lead · id of lead |
userId | uuid | Owner |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
lead_not_found | The lead must exist. |
not_a_member | The owner must be an active member of this workspace. |
Events it publishes: crm.lead_assigned
What it writes: lead (update) — Sets the owner
curl -X POST https://app.getera.io/api/v1/leads/LEAD_ID/actions/assign_lead \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"userId":"USER_ID"}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_assigned"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.assignLead("LEAD_ID", {
userId: "USER_ID"
})era leads assign_lead LEAD_ID \
--user-id "USER_ID"{
"name": "era__crm__assign_lead",
"arguments": {
"id": "LEAD_ID",
"userId": "USER_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 lead_not_found, not_a_member · 429 slow down
crm.attribute_lead
Credit a lead to an affiliate — Says which partner sent the lead; moving existing credit has to be confirmed.
Needs crm:write · changes data
https://app.getera.io/api/v1/leads/{id}/actions/attribute_lead- Canonical address:
POST /api/v1/actions/crm.attribute_lead
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Lead · id of lead |
affiliateId | uuid | Affiliate · id of affiliate | |
replace | boolean | Move existing credit |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
lead_not_found | The lead must exist. |
lead_archived | An archived lead is restored first. |
affiliate_not_found | The affiliate must exist and be open. |
already_attributed | Moving credit from one affiliate to another must be confirmed. |
unchanged | The lead must not already be credited that way. |
Events it publishes: crm.lead_attributed
What it writes: lead (update) — Sets who referred the lead
curl -X POST https://app.getera.io/api/v1/leads/LEAD_ID/actions/attribute_lead \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"affiliateId":"AFFILIATE_ID"}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_attributed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.attributeLead("LEAD_ID", {
affiliateId: "AFFILIATE_ID"
})era leads attribute_lead LEAD_ID \
--affiliate-id "AFFILIATE_ID"{
"name": "era__crm__attribute_lead",
"arguments": {
"id": "LEAD_ID",
"affiliateId": "AFFILIATE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 lead_not_found, lead_archived, affiliate_not_found, already_attributed, unchanged · 429 slow down
crm.cancel_lead
Cancel a lead — Records that the customer withdrew after buying; finance handles the refund.
Needs crm:write · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/lead_cancellations/actions/cancel_lead- Canonical address:
POST /api/v1/actions/crm.cancel_lead
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Lead · id of lead |
reason | string | yes | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
lead_not_found | The lead must exist. |
already_cancelled | A lead is cancelled once. |
Events it publishes: crm.lead_cancelled
What it writes: lead_cancellation (insert) — Records the cancellation
curl -X POST https://app.getera.io/api/v1/lead_cancellations/actions/cancel_lead \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"id":"LEAD_ID","reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_cancelled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leadCancellations.cancelLead({
id: "LEAD_ID",
reason: "Reason"
})era lead_cancellations cancel_lead \
--id "LEAD_ID" \
--reason "Reason"{
"name": "era__crm__cancel_lead",
"arguments": {
"id": "LEAD_ID",
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 lead_not_found, already_cancelled · 429 slow down
crm.capture_lead
Open a lead — Starts a sales pursuit for somebody already recognised as a customer.
Needs crm:write · changes data
https://app.getera.io/api/v1/leads/actions/capture_lead- REST alias:
POST /api/v1/leads - Canonical address:
POST /api/v1/actions/crm.capture_lead
Input
| Field | Type | Required | Notes |
|---|---|---|---|
customerId | uuid | yes | Person · id of customer |
sourceId | uuid | Source · id of lead_source | |
stageId | uuid | Stage · id of lead_stage | |
assignedUserId | uuid | Assigned to | |
assignmentNote | string | Note for the owner · max 2000 | |
message | string | What they said · max 4000 | |
affiliateId | uuid | Referred by · id of affiliate | |
trialDurationDays | number | Trial length (days) · min 0 · max 365 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
customer_not_found | The person must already exist as a customer. |
customer_archived | An archived person cannot be pursued. |
no_open_stage | The workspace must have at least one open stage. |
stage_not_available | The stage must exist and be active. |
source_not_available | The source must exist and be active. |
affiliate_not_found | The referring affiliate must exist. |
Events it publishes: crm.lead_captured
What it writes: lead (insert) — Opens the lead
curl -X POST https://app.getera.io/api/v1/leads/actions/capture_lead \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customerId":"CUSTOMER_ID"}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_captured"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.create({
customerId: "CUSTOMER_ID"
})era leads create \
--customer-id "CUSTOMER_ID"{
"name": "era__crm__capture_lead",
"arguments": {
"customerId": "CUSTOMER_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 customer_not_found, customer_archived, no_open_stage, stage_not_available, source_not_available, affiliate_not_found · 429 slow down
crm.create_call_template
Save a call template — A quick note the call form can drop in — new, or an existing one edited and reopened.
Needs crm:manage · changes data
https://app.getera.io/api/v1/call_templates/{id}/actions/create_call_template- Canonical address:
POST /api/v1/actions/crm.create_call_template
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Template · id of call_template | |
name | string | yes | Name · max 255 |
templateText | string | yes | Text · max 8000 |
isActive | boolean | In use |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | An edited template must exist. |
Events it publishes: crm.call_template_changed
What it writes: call_template (insert) — Adds the template; call_template (update) — Or rewrites an existing one, opening or closing it
curl -X POST https://app.getera.io/api/v1/call_templates/CALL_TEMPLATE_ID/actions/create_call_template \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name","templateText":"Text"}'{
"ok": true,
"result": "…",
"events": [
"crm.call_template_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.callTemplates.createCallTemplate("CALL_TEMPLATE_ID", {
name: "Name",
templateText: "Text"
})era call_templates create_call_template CALL_TEMPLATE_ID \
--name "Name" \
--template-text "Text"{
"name": "era__crm__create_call_template",
"arguments": {
"id": "CALL_TEMPLATE_ID",
"name": "Name",
"templateText": "Text"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found · 429 slow down
crm.deactivate_affiliate
Close an affiliate — Stops new referrals; open leads keep their attribution.
Needs crm:manage · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/affiliates/{id}/actions/deactivate_affiliate- Canonical address:
POST /api/v1/actions/crm.deactivate_affiliate
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Affiliate · id of affiliate |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The affiliate must exist. |
already_archived | The affiliate must still be open. |
Events it publishes: crm.affiliate_deactivated
What it writes: affiliate (update) — Closes the affiliate
curl -X POST https://app.getera.io/api/v1/affiliates/AFFILIATE_ID/actions/deactivate_affiliate \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"crm.affiliate_deactivated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.affiliates.deactivateAffiliate("AFFILIATE_ID")era affiliates deactivate_affiliate AFFILIATE_ID{
"name": "era__crm__deactivate_affiliate",
"arguments": {
"id": "AFFILIATE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_archived · 429 slow down
crm.deactivate_call_template
Close a call template — Takes a template out of the picker.
Needs crm:manage · changes data
https://app.getera.io/api/v1/call_templates/{id}/actions/deactivate_call_template- Canonical address:
POST /api/v1/actions/crm.deactivate_call_template
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Template · id of call_template |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The template must exist. |
already_inactive | The template must still be open. |
Events it publishes: crm.call_template_changed
What it writes: call_template (update) — Closes the template
curl -X POST https://app.getera.io/api/v1/call_templates/CALL_TEMPLATE_ID/actions/deactivate_call_template \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"crm.call_template_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.callTemplates.deactivateCallTemplate("CALL_TEMPLATE_ID")era call_templates deactivate_call_template CALL_TEMPLATE_ID{
"name": "era__crm__deactivate_call_template",
"arguments": {
"id": "CALL_TEMPLATE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_inactive · 429 slow down
crm.deactivate_lost_reason
Close a loss reason — Takes a reason out of the picker without touching the leads that used it.
Needs crm:manage · changes data
https://app.getera.io/api/v1/lost_reasons/{id}/actions/deactivate_lost_reason- Canonical address:
POST /api/v1/actions/crm.deactivate_lost_reason
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Reason · id of lost_reason |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The reason must exist. |
already_inactive | The reason must still be open. |
Events it publishes: crm.lost_reason_changed
What it writes: lost_reason (update) — Closes the reason
curl -X POST https://app.getera.io/api/v1/lost_reasons/LOST_REASON_ID/actions/deactivate_lost_reason \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"crm.lost_reason_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.lostReasons.deactivateLostReason("LOST_REASON_ID")era lost_reasons deactivate_lost_reason LOST_REASON_ID{
"name": "era__crm__deactivate_lost_reason",
"arguments": {
"id": "LOST_REASON_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_inactive · 429 slow down
crm.export_calls
Export calls to Excel — Returns the call log grouped into one workbook sheet per caller.
Needs crm:export · reaches outside (sends, publishes or charges)
https://app.getera.io/api/v1/actions/crm.export_callsInput
| Field | Type | Required | Notes |
|---|---|---|---|
outcome | uuid | Outcome · id of call_status | |
caller | string | Called by · max 64 | |
from | date | Called from | |
to | date | Called until |
Events it publishes: crm.calls_exported
curl -X POST https://app.getera.io/api/v1/actions/crm.export_calls \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"outcome":"CALL_STATUS_ID"}'{
"ok": true,
"result": "…",
"events": [
"crm.calls_exported"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.actions.run("crm.export_calls", {
outcome: "CALL_STATUS_ID"
})era crm export_calls \
--outcome "CALL_STATUS_ID"{
"name": "era__crm__export_calls",
"arguments": {
"outcome": "CALL_STATUS_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:export · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 429 slow down
crm.export_leads
Export leads — The lead list as filtered, as rows — the leads screen turns them into a CSV.
Needs crm:export · reaches outside (sends, publishes or charges)
https://app.getera.io/api/v1/actions/crm.export_leadsInput
| Field | Type | Required | Notes |
|---|---|---|---|
view | string (enum) | View · Allowed values: all, trials, recurring, today, tomorrow, overdue, never, archived | |
q | string | Search · max 200 | |
stageId | uuid | Stage · id of lead_stage | |
sourceId | uuid | Source · id of lead_source | |
ownerId | string | Owner · max 64 | |
createdFrom | date | Added from | |
createdTo | date | Added until | |
leadIds | object / array | Only these leads |
Events it publishes: crm.leads_exported
curl -X POST https://app.getera.io/api/v1/actions/crm.export_leads \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"view":"all"}'{
"ok": true,
"result": "…",
"events": [
"crm.leads_exported"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.actions.run("crm.export_leads", {
view: "all"
})era crm export_leads \
--view "all"{
"name": "era__crm__export_leads",
"arguments": {
"view": "all"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:export · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 429 slow down
crm.issue_affiliate_report_link
Send an affiliate their report link — A link the partner opens without signing in to see their leads and commission. Shown once.
Needs crm:manage · changes data
https://app.getera.io/api/v1/affiliate_report_links/actions/issue_affiliate_report_link- Canonical address:
POST /api/v1/actions/crm.issue_affiliate_report_link
Input
| Field | Type | Required | Notes |
|---|---|---|---|
affiliateId | uuid | yes | Affiliate · id of affiliate |
purpose | string (enum) | Link for · Allowed values: report, referral | |
expiresInDays | number | Open for (days) · min 1 · max 365 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
affiliate_not_found | The affiliate must exist and be open. |
Events it publishes: crm.affiliate_report_link_issued
What it writes: affiliate_report_link (insert) — Stores the hash of a new token
curl -X POST https://app.getera.io/api/v1/affiliate_report_links/actions/issue_affiliate_report_link \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"affiliateId":"AFFILIATE_ID"}'{
"ok": true,
"result": "…",
"events": [
"crm.affiliate_report_link_issued"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.affiliateReportLinks.issueAffiliateReportLink({
affiliateId: "AFFILIATE_ID"
})era affiliate_report_links issue_affiliate_report_link \
--affiliate-id "AFFILIATE_ID"{
"name": "era__crm__issue_affiliate_report_link",
"arguments": {
"affiliateId": "AFFILIATE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 affiliate_not_found · 429 slow down
crm.log_call
Log a call — Records a call — and closes the lead when it is the third failure in a row.
Needs crm:write · changes data
https://app.getera.io/api/v1/leads/{id}/actions/log_call- Canonical address:
POST /api/v1/actions/crm.log_call
Input
| Field | Type | Required | Notes |
|---|---|---|---|
leadId | uuid | yes | Filled from the path {id} · Lead · id of lead |
statusId | uuid | yes | Outcome · id of call_status |
resultId | uuid | yes | Result · id of call_result |
durationMinutes | number | Duration (minutes) · min 0 · max 1440 | |
notes | string | Notes · max 8000 | |
isImportant | boolean | Important | |
followUpDate | date | Follow up on | |
followUpTime | string | At · max 8 | |
followUpNotes | string | Follow-up notes · max 4000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
lead_not_found | The lead must exist. |
lead_archived | An archived lead takes no calls. |
call_status_not_available | The outcome must exist and be active. |
call_result_not_available | The result must exist and be active. |
follow_up_in_the_past | A follow-up must not be scheduled in the past. |
Events it publishes: crm.call_logged, crm.lead_stage_changed, crm.lead_lost
What it writes: call (insert) — Records the call; lead (update) — Closes the lead on the Nth consecutive failure
curl -X POST https://app.getera.io/api/v1/leads/LEAD_ID/actions/log_call \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"statusId":"CALL_STATUS_ID","resultId":"CALL_RESULT_ID"}'{
"ok": true,
"result": "…",
"events": [
"crm.call_logged",
"crm.lead_stage_changed",
"crm.lead_lost"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.logCall("LEAD_ID", {
statusId: "CALL_STATUS_ID",
resultId: "CALL_RESULT_ID"
})era leads log_call LEAD_ID \
--status-id "CALL_STATUS_ID" \
--result-id "CALL_RESULT_ID"{
"name": "era__crm__log_call",
"arguments": {
"leadId": "LEAD_ID",
"statusId": "CALL_STATUS_ID",
"resultId": "CALL_RESULT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 lead_not_found, lead_archived, call_status_not_available, call_result_not_available, follow_up_in_the_past · 429 slow down
crm.merge_lost_reasons
Merge loss reasons — Moves every lead filed under several reasons onto one, then closes the others.
Needs crm:manage · removes or cannot be undone
https://app.getera.io/api/v1/lost_reasons/{id}/actions/merge_lost_reasons- Canonical address:
POST /api/v1/actions/crm.merge_lost_reasons
Input
| Field | Type | Required | Notes |
|---|---|---|---|
sourceIds | object / array | yes | Reasons to merge |
targetId | uuid | yes | Filled from the path {id} · Keep this one · id of lost_reason |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_sources | At least one reason other than the target must be picked. |
target_not_found | The reason everything moves onto must be open. |
Events it publishes: crm.lost_reason_changed
What it writes: lead (update) — Re-files the leads onto the surviving reason; lost_reason (update) — Closes the reasons that were folded in
curl -X POST https://app.getera.io/api/v1/lost_reasons/LOST_REASON_ID/actions/merge_lost_reasons \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"sourceIds":{}}'{
"ok": true,
"result": "…",
"events": [
"crm.lost_reason_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.lostReasons.mergeLostReasons("LOST_REASON_ID", {
sourceIds: {}
})era lost_reasons merge_lost_reasons LOST_REASON_ID --data '{"sourceIds":{}}'{
"name": "era__crm__merge_lost_reasons",
"arguments": {
"targetId": "LOST_REASON_ID",
"sourceIds": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_sources, target_not_found · 429 slow down
crm.raise_stale_leads
Flag quiet leads — Machine-only (the daily routine): every open lead that has not moved for the window is told about once.
Needs crm:write · changes data
403 machine_only).https://app.getera.io/api/v1/leads/actions/raise_stale_leads- Canonical address:
POST /api/v1/actions/crm.raise_stale_leads
Input
This action takes no input.
Events it publishes: crm.lead_stale
What it writes: lead (update) — Records that the lead was flagged as quiet
Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 429 slow down
crm.restore_lead
Restore a lead — Brings an archived lead back.
Needs crm:delete · changes data
https://app.getera.io/api/v1/leads/{id}/actions/restore_lead- Canonical address:
POST /api/v1/actions/crm.restore_lead
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Lead · id of lead |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
lead_not_found | The lead must exist. |
not_archived | The lead must be archived. |
Events it publishes: crm.lead_restored
What it writes: lead (update) — Clears archived_at
curl -X POST https://app.getera.io/api/v1/leads/LEAD_ID/actions/restore_lead \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_restored"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.restoreLead("LEAD_ID")era leads restore_lead LEAD_ID{
"name": "era__crm__restore_lead",
"arguments": {
"id": "LEAD_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:delete · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 lead_not_found, not_archived · 429 slow down
crm.revoke_affiliate_report_link
Close an affiliate report link — The link stops working at once; the partner can be sent a new one.
Needs crm:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/affiliate_report_links/{id}/actions/revoke_affiliate_report_link- Canonical address:
POST /api/v1/actions/crm.revoke_affiliate_report_link
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Link · id of affiliate_report_link |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The link must exist. |
already_revoked | The link must still be open. |
Events it publishes: crm.affiliate_report_link_revoked
What it writes: affiliate_report_link (update) — Stamps it revoked
curl -X POST https://app.getera.io/api/v1/affiliate_report_links/AFFILIATE_REPORT_LINK_ID/actions/revoke_affiliate_report_link \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"crm.affiliate_report_link_revoked"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.affiliateReportLinks.revokeAffiliateReportLink("AFFILIATE_REPORT_LINK_ID")era affiliate_report_links revoke_affiliate_report_link AFFILIATE_REPORT_LINK_ID{
"name": "era__crm__revoke_affiliate_report_link",
"arguments": {
"id": "AFFILIATE_REPORT_LINK_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_revoked · 429 slow down
crm.save_affiliate
Add or edit an affiliate — The referral channel and the terms it was agreed on.
Needs crm:manage · changes data
https://app.getera.io/api/v1/affiliates/{id}/actions/save_affiliate- REST alias:
PATCH /api/v1/affiliates/{id} - Canonical address:
POST /api/v1/actions/crm.save_affiliate
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Affiliate · id of affiliate | |
name | string | yes | Name · max 200 |
email | |||
phone | phone | Phone | |
commissionType | string (enum) | yes | Commission type · Allowed values: percentage, fixed |
commissionRate | number | Rate (%) · min 0 · max 100 | |
fixedCommissionAmount | number (money) | Fixed amount · min 0 | |
commissionCurrency | string | Currency · max 3 | |
commissionAccrualMode | string (enum) | Earned · Allowed values: proportional, on_sale, on_full_collection | |
notes | string | Notes · max 4000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_contact | An affiliate needs an email address or a phone number. |
rate_required | A percentage arrangement needs a rate. |
amount_required | A fixed arrangement needs an amount. |
Events it publishes: crm.affiliate_created, crm.affiliate_updated
What it writes: affiliate (upsert) — Writes the affiliate
curl -X POST https://app.getera.io/api/v1/affiliates/AFFILIATE_ID/actions/save_affiliate \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name","commissionType":"percentage"}'{
"ok": true,
"result": "…",
"events": [
"crm.affiliate_created",
"crm.affiliate_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.affiliates.saveAffiliate("AFFILIATE_ID", {
name: "Name",
commissionType: "percentage"
})era affiliates save_affiliate AFFILIATE_ID \
--name "Name" \
--commission-type "percentage"{
"name": "era__crm__save_affiliate",
"arguments": {
"id": "AFFILIATE_ID",
"name": "Name",
"commissionType": "percentage"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_contact, rate_required, amount_required · 429 slow down
crm.score_leads
Score leads — Gives every open lead a 0–100 score from its contact details, stage, calls, value and recency, with the reasons.
Needs crm:write · changes data
https://app.getera.io/api/v1/leads/actions/score_leads- Canonical address:
POST /api/v1/actions/crm.score_leads
Input
| Field | Type | Required | Notes |
|---|---|---|---|
leadIds | object / array | Only these leads |
Events it publishes: crm.leads_scored
What it writes: lead (update) — Writes score, score_reasons and scored_at
curl -X POST https://app.getera.io/api/v1/leads/actions/score_leads \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"leadIds":{}}'{
"ok": true,
"result": "…",
"events": [
"crm.leads_scored"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.scoreLeads({
leadIds: {}
})era leads score_leads --data '{"leadIds":{}}'{
"name": "era__crm__score_leads",
"arguments": {
"leadIds": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 429 slow down
crm.set_call_option_active
Open or close a call option — Takes a call outcome or result out of the picker, without touching the calls that used it.
Needs crm:manage · changes data
https://app.getera.io/api/v1/call_statuses/actions/set_call_option_active- Canonical address:
POST /api/v1/actions/crm.set_call_option_active
Input
| Field | Type | Required | Notes |
|---|---|---|---|
table | string (enum) | yes | Vocabulary · Allowed values: call_status, call_result |
id | uuid | yes | Option |
active | boolean | yes | Open |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The option must exist. |
already_in_that_state | The option is already open, or already closed. |
Events it publishes: crm.call_dictionary_changed
What it writes: call_status (update) — Opens or closes the outcome; call_result (update) — Opens or closes the result
curl -X POST https://app.getera.io/api/v1/call_statuses/actions/set_call_option_active \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"table":"call_status","id":"ID_ID","active":true}'{
"ok": true,
"result": "…",
"events": [
"crm.call_dictionary_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.callStatuses.setCallOptionActive({
table: "call_status",
id: "ID_ID",
active: true
})era call_statuses set_call_option_active \
--table "call_status" \
--id "ID_ID" \
--active true{
"name": "era__crm__set_call_option_active",
"arguments": {
"table": "call_status",
"id": "ID_ID",
"active": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_in_that_state · 429 slow down
crm.set_call_result
Add or change a call result — Manages the call results, and whether each one counts for or against the lead.
Needs crm:manage · changes data
https://app.getera.io/api/v1/call_results/{id}/actions/set_call_result- Canonical address:
POST /api/v1/actions/crm.set_call_result
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Result · id of call_result | |
key | string | yes | Key · max 50 |
name | string | yes | Name · max 255 |
behavior | string (enum) | yes | Counts as · Allowed values: positive, neutral, negative |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
key_taken | Two results may not share a key. |
Events it publishes: crm.call_dictionary_changed
What it writes: call_result (upsert) — Writes the result
curl -X POST https://app.getera.io/api/v1/call_results/CALL_RESULT_ID/actions/set_call_result \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"key":"Key","name":"Name","behavior":"positive"}'{
"ok": true,
"result": "…",
"events": [
"crm.call_dictionary_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.callResults.setCallResult("CALL_RESULT_ID", {
key: "Key",
name: "Name",
behavior: "positive"
})era call_results set_call_result CALL_RESULT_ID \
--key "Key" \
--name "Name" \
--behavior "positive"{
"name": "era__crm__set_call_result",
"arguments": {
"id": "CALL_RESULT_ID",
"key": "Key",
"name": "Name",
"behavior": "positive"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 key_taken · 429 slow down
crm.set_call_status
Add or change a call outcome — Manages the call outcomes, and whether each one counts as reaching somebody.
Needs crm:manage · changes data
https://app.getera.io/api/v1/call_statuses/{id}/actions/set_call_status- Canonical address:
POST /api/v1/actions/crm.set_call_status
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Outcome · id of call_status | |
key | string | yes | Key · max 50 |
name | string | yes | Name · max 255 |
behavior | string (enum) | yes | Counts as · Allowed values: reached, not_reached |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
key_taken | Two outcomes may not share a key. |
Events it publishes: crm.call_dictionary_changed
What it writes: call_status (upsert) — Writes the outcome
curl -X POST https://app.getera.io/api/v1/call_statuses/CALL_STATUSE_ID/actions/set_call_status \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"key":"Key","name":"Name","behavior":"reached"}'{
"ok": true,
"result": "…",
"events": [
"crm.call_dictionary_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.callStatuses.setCallStatus("CALL_STATUSE_ID", {
key: "Key",
name: "Name",
behavior: "reached"
})era call_statuses set_call_status CALL_STATUSE_ID \
--key "Key" \
--name "Name" \
--behavior "reached"{
"name": "era__crm__set_call_status",
"arguments": {
"id": "CALL_STATUSE_ID",
"key": "Key",
"name": "Name",
"behavior": "reached"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 key_taken · 429 slow down
crm.set_lead_source
Add or rename a lead source — Manages where leads come from — one row per channel, not 25 spellings.
Needs crm:manage · changes data
https://app.getera.io/api/v1/lead_sources/{id}/actions/set_lead_source- Canonical address:
POST /api/v1/actions/crm.set_lead_source
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Source · id of lead_source | |
key | string | yes | Key · max 50 |
name | string | yes | Name · max 255 |
displayOrder | number | Order · min 0 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
key_taken | Two sources may not share a key. |
Events it publishes: crm.lead_source_changed
What it writes: lead_source (upsert) — Writes the source
curl -X POST https://app.getera.io/api/v1/lead_sources/LEAD_SOURCE_ID/actions/set_lead_source \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"key":"Key","name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_source_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leadSources.setLeadSource("LEAD_SOURCE_ID", {
key: "Key",
name: "Name"
})era lead_sources set_lead_source LEAD_SOURCE_ID \
--key "Key" \
--name "Name"{
"name": "era__crm__set_lead_source",
"arguments": {
"id": "LEAD_SOURCE_ID",
"key": "Key",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 key_taken · 429 slow down
crm.set_lead_source_active
Open or close a lead source — Takes a channel out of the pickers without touching the leads that came from it.
Needs crm:manage · changes data
https://app.getera.io/api/v1/lead_sources/{id}/actions/set_lead_source_active- Canonical address:
POST /api/v1/actions/crm.set_lead_source_active
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Source · id of lead_source |
active | boolean | yes | Open |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The source must exist. |
already_in_that_state | The source is already open, or already closed. |
Events it publishes: crm.lead_source_changed
What it writes: lead_source (update) — Opens or closes the source
curl -X POST https://app.getera.io/api/v1/lead_sources/LEAD_SOURCE_ID/actions/set_lead_source_active \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"active":true}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_source_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leadSources.setLeadSourceActive("LEAD_SOURCE_ID", {
active: true
})era lead_sources set_lead_source_active LEAD_SOURCE_ID \
--active true{
"name": "era__crm__set_lead_source_active",
"arguments": {
"id": "LEAD_SOURCE_ID",
"active": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_in_that_state · 429 slow down
crm.set_lead_stage
Move a lead — Moves the lead to another stage, and closes it when that stage is terminal.
Needs crm:write · changes data
https://app.getera.io/api/v1/leads/{id}/actions/set_lead_stage- Canonical address:
POST /api/v1/actions/crm.set_lead_stage
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Lead · id of lead |
stageId | uuid | yes | Stage · id of lead_stage |
lostReasonId | uuid | Reason · id of lost_reason |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
lead_not_found | The lead must exist. |
lead_archived | An archived lead is restored before it is moved. |
stage_not_available | The stage must exist and be active. |
lost_reason_required | Closing a lead as lost requires a reason. |
lost_reason_not_available | The reason must exist and be active. |
Events it publishes: crm.lead_stage_changed, crm.lead_won, crm.lead_lost
What it writes: lead (update) — Moves the lead and stamps its closure
curl -X POST https://app.getera.io/api/v1/leads/LEAD_ID/actions/set_lead_stage \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"stageId":"LEAD_STAGE_ID"}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_stage_changed",
"crm.lead_won",
"crm.lead_lost"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.setLeadStage("LEAD_ID", {
stageId: "LEAD_STAGE_ID"
})era leads set_lead_stage LEAD_ID \
--stage-id "LEAD_STAGE_ID"{
"name": "era__crm__set_lead_stage",
"arguments": {
"id": "LEAD_ID",
"stageId": "LEAD_STAGE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 lead_not_found, lead_archived, stage_not_available, lost_reason_required, lost_reason_not_available · 429 slow down
crm.set_lead_stage_active
Open or close a stage — Takes a stage out of the pickers; leads already on it stay there.
Needs crm:manage · changes data
https://app.getera.io/api/v1/lead_stages/{id}/actions/set_lead_stage_active- Canonical address:
POST /api/v1/actions/crm.set_lead_stage_active
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Stage · id of lead_stage |
active | boolean | yes | Open |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The stage must exist. |
already_in_that_state | The stage is already open, or already closed. |
last_terminal_stage | A workspace keeps at least one won stage and one lost stage. |
last_open_stage | A workspace keeps at least one open stage for new leads. |
Events it publishes: crm.lead_stage_definition_changed
What it writes: lead_stage (update) — Opens or closes the stage
curl -X POST https://app.getera.io/api/v1/lead_stages/LEAD_STAGE_ID/actions/set_lead_stage_active \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"active":true}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_stage_definition_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leadStages.setLeadStageActive("LEAD_STAGE_ID", {
active: true
})era lead_stages set_lead_stage_active LEAD_STAGE_ID \
--active true{
"name": "era__crm__set_lead_stage_active",
"arguments": {
"id": "LEAD_STAGE_ID",
"active": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_in_that_state, last_terminal_stage, last_open_stage · 429 slow down
crm.set_lead_stage_definition
Add or change a stage — Manages the pipeline — and keeps at least one won and one lost stage.
Needs crm:manage · changes data
https://app.getera.io/api/v1/lead_stages/{id}/actions/set_lead_stage_definition- Canonical address:
POST /api/v1/actions/crm.set_lead_stage_definition
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Stage · id of lead_stage | |
key | string | yes | Key · max 50 |
name | string | yes | Name · max 255 |
behavior | string (enum) | yes | Means · Allowed values: open, won, lost, suspended |
displayOrder | number | Order · min 0 | |
winProbability | number | Chance of winning (%) · min 0 · max 100 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The stage must exist. |
key_taken | Two stages may not share a key. |
last_terminal_stage | A workspace keeps at least one won stage and one lost stage. |
Events it publishes: crm.lead_stage_definition_changed
What it writes: lead_stage (upsert) — Writes the stage
curl -X POST https://app.getera.io/api/v1/lead_stages/LEAD_STAGE_ID/actions/set_lead_stage_definition \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"key":"Key","name":"Name","behavior":"open"}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_stage_definition_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leadStages.setLeadStageDefinition("LEAD_STAGE_ID", {
key: "Key",
name: "Name",
behavior: "open"
})era lead_stages set_lead_stage_definition LEAD_STAGE_ID \
--key "Key" \
--name "Name" \
--behavior "open"{
"name": "era__crm__set_lead_stage_definition",
"arguments": {
"id": "LEAD_STAGE_ID",
"key": "Key",
"name": "Name",
"behavior": "open"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, key_taken, last_terminal_stage · 429 slow down
crm.set_lost_reason
Add or rename a loss reason — Manages the workspace’s own vocabulary for why a lead was lost.
Needs crm:manage · changes data
https://app.getera.io/api/v1/lost_reasons/{id}/actions/set_lost_reason- Canonical address:
POST /api/v1/actions/crm.set_lost_reason
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Reason · id of lost_reason | |
name | string | yes | Name · max 255 |
displayOrder | number | Order · min 0 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
name_taken | Two reasons may not share a name. |
Events it publishes: crm.lost_reason_changed
What it writes: lost_reason (upsert) — Writes the reason
curl -X POST https://app.getera.io/api/v1/lost_reasons/LOST_REASON_ID/actions/set_lost_reason \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"crm.lost_reason_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.lostReasons.setLostReason("LOST_REASON_ID", {
name: "Name"
})era lost_reasons set_lost_reason LOST_REASON_ID \
--name "Name"{
"name": "era__crm__set_lost_reason",
"arguments": {
"id": "LOST_REASON_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 name_taken · 429 slow down
crm.start_trial
Start a trial — Begins the trial and records the day it began.
Needs crm:write · changes data
https://app.getera.io/api/v1/leads/{id}/actions/start_trial- Canonical address:
POST /api/v1/actions/crm.start_trial
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Lead · id of lead |
days | number | Length (days) · min 1 · max 365 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
lead_not_found | The lead must exist. |
lead_archived | An archived lead cannot start a trial. |
no_trial_length | A trial needs a length in days. |
Events it publishes: crm.lead_trial_started
What it writes: lead (update) — Records the trial and its start date
curl -X POST https://app.getera.io/api/v1/leads/LEAD_ID/actions/start_trial \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"days":1}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_trial_started"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.startTrial("LEAD_ID", {
days: 1
})era leads start_trial LEAD_ID \
--days 1{
"name": "era__crm__start_trial",
"arguments": {
"id": "LEAD_ID",
"days": 1
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 lead_not_found, lead_archived, no_trial_length · 429 slow down
crm.update_lead
Edit a lead — Changes the pursuit — its source, owner, note or trial length.
Needs crm:write · changes data
https://app.getera.io/api/v1/leads/{id}/actions/update_lead- REST alias:
PATCH /api/v1/leads/{id} - Canonical address:
POST /api/v1/actions/crm.update_lead
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Lead · id of lead |
sourceId | uuid | Source · id of lead_source | |
assignedUserId | uuid | Assigned to | |
assignmentNote | string | Note for the owner · max 2000 | |
message | string | What they said · max 4000 | |
affiliateId | uuid | Referred by · id of affiliate | |
trialDurationDays | number | Trial length (days) · min 0 · max 365 | |
expectedValueAmount | number (money) | Deal value | |
expectedValueCurrency | string | Deal currency · max 3 | |
expectedCloseDate | date | Expected close |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
lead_not_found | The lead must exist. |
lead_archived | An archived lead is restored before it is edited. |
invalid_currency | A deal value’s currency is a three-letter code. |
source_not_available | The source must exist and be active. |
affiliate_not_found | The referring affiliate must exist. |
not_a_member | The owner must be an active member of this workspace. |
Events it publishes: crm.lead_updated
What it writes: lead (update) — Updates the lead
curl -X POST https://app.getera.io/api/v1/leads/LEAD_ID/actions/update_lead \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"sourceId":"LEAD_SOURCE_ID"}'{
"ok": true,
"result": "…",
"events": [
"crm.lead_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.leads.updateLead("LEAD_ID", {
sourceId: "LEAD_SOURCE_ID"
})era leads update_lead LEAD_ID \
--source-id "LEAD_SOURCE_ID"{
"name": "era__crm__update_lead",
"arguments": {
"id": "LEAD_ID",
"sourceId": "LEAD_SOURCE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold crm:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 lead_not_found, lead_archived, invalid_currency, source_not_available, affiliate_not_found, not_a_member · 429 slow down
Data models
AffiliateReportLink
Table affiliate_report_link · Resource affiliate_report_links
GET /api/v1/affiliate_report_linksGET /api/v1/affiliate_report_links/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
affiliate_id | string (uuid) · uuid | affiliate | ||
expires_at | string (date-time) · timestamptz | yes | ||
revoked_at | string (date-time) · timestamptz | yes | ||
last_used_at | string (date-time) · timestamptz | yes | ||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
purpose | string · text | report, referral |
Primary key: id
Affiliate
Table affiliate · Resource affiliates
GET /api/v1/affiliatesGET /api/v1/affiliates/{id}PATCH /api/v1/affiliates/{id}→crm.save_affiliate
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
email | string · text | yes | ||
phone | string · text | yes | ||
phone_country_code | integer · int2 | yes | ||
status | string · text | active, inactive | ||
notes | string · text | yes | ||
commission_type | string · text | percentage, fixed | ||
commission_rate | string (decimal) · numeric | yes | ||
fixed_commission_amount | string (int64) · int8 | yes | ||
commission_currency | string · bpchar | max 3 | ||
commission_accrual_mode | string · text | proportional, on_sale, on_full_collection | ||
archived_at | string (date-time) · timestamptz | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK (((commission_rate >= (0)::numeric) AND (commission_rate <= (100)::numeric)))CHECK ((fixed_commission_amount >= 0))CHECK ((((commission_type = 'percentage'::text) AND (commission_rate IS NOT NULL)) OR ((commission_type = 'fixed'::text) AND (fixed_commission_amount IS NOT NULL))))
CallResult
Table call_result · Resource call_results
GET /api/v1/call_resultsGET /api/v1/call_results/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
key | string · text | |||
name | string · text | |||
behavior | string · text | positive, neutral, negative | ||
display_order | integer · int4 | |||
is_active | boolean · bool | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
CallStatus
Table call_status · Resource call_statuses
GET /api/v1/call_statusesGET /api/v1/call_statuses/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
key | string · text | |||
name | string · text | |||
behavior | string · text | reached, not_reached | ||
display_order | integer · int4 | |||
is_active | boolean · bool | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
CallTemplate
Table call_template · Resource call_templates
GET /api/v1/call_templatesGET /api/v1/call_templates/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
template_text | string · text | |||
is_active | boolean · bool | |||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Call
Table call · Resource calls
GET /api/v1/callsGET /api/v1/calls/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
lead_id | string (uuid) · uuid | lead | ||
called_by_user_id | string (uuid) · uuid | yes | app_user | |
assigned_user_id | string (uuid) · uuid | yes | app_user | |
duration_minutes | integer · int4 | yes | ||
status_id | string (uuid) · uuid | call_status | ||
result_id | string (uuid) · uuid | call_result | ||
notes | string · text | yes | ||
is_important | boolean · bool | |||
follow_up_date | string (date) · date | yes | ||
follow_up_time | string (time) · time | yes | ||
follow_up_notes | string · text | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((duration_minutes >= 0))
LeadCancellation
Table lead_cancellation · Resource lead_cancellations
GET /api/v1/lead_cancellationsGET /api/v1/lead_cancellations/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
lead_id | string (uuid) · uuid | lead | ||
reason | string · text | |||
cancelled_by_user_id | string (uuid) · uuid | yes | app_user | |
cancellation_type | string · text | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
LeadSource
Table lead_source · Resource lead_sources
GET /api/v1/lead_sourcesGET /api/v1/lead_sources/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
key | string · text | |||
name | string · text | |||
display_order | integer · int4 | |||
is_active | boolean · bool | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
LeadStage
Table lead_stage · Resource lead_stages
GET /api/v1/lead_stagesGET /api/v1/lead_stages/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
key | string · text | |||
name | string · text | |||
behavior | string · text | open, won, lost, suspended | ||
display_order | integer · int4 | |||
is_active | boolean · bool | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
win_probability | integer · int2 | yes |
Primary key: id
Checks:
CHECK (((win_probability IS NULL) OR ((win_probability >= 0) AND (win_probability <= 100))))
Lead
Table lead · Resource leads
GET /api/v1/leadsGET /api/v1/leads/{id}POST /api/v1/leads→crm.capture_leadPATCH /api/v1/leads/{id}→crm.update_leadDELETE /api/v1/leads/{id}→crm.archive_lead
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
customer_id | string (uuid) · uuid | customer | ||
source_id | string (uuid) · uuid | yes | lead_source | |
stage_id | string (uuid) · uuid | lead_stage | ||
lost_reason_id | string (uuid) · uuid | yes | lost_reason | |
affiliate_id | string (uuid) · uuid | yes | affiliate | |
assigned_user_id | string (uuid) · uuid | yes | app_user | |
assignment_note | string · text | yes | ||
message | string · text | yes | ||
trial_duration_days | integer · int4 | |||
trial_start_date | string (date) · date | yes | ||
archived_at | string (date-time) · timestamptz | yes | ||
closed_at | string (date-time) · timestamptz | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
expected_value_amount | string (int64) · int8 | yes | ||
expected_value_currency | string · text | yes | ||
expected_close_date | string (date) · date | yes | ||
stale_notice_at | string (date-time) · timestamptz | yes | ||
score | integer · int2 | yes | ||
scored_at | string (date-time) · timestamptz | yes | ||
score_reasons | object · jsonb |
Primary key: id
Checks:
CHECK ((((expected_value_amount IS NULL) OR (expected_value_amount >= 0)) AND ((expected_value_currency IS NULL) OR (expected_value_currency ~ '^[A-Z]{3}$'::text)) AND ((expected_value_amount IS NULL) OR (expected_value_currency IS NOT NULL))))CHECK (((score IS NULL) OR ((score >= 0) AND (score <= 100))))CHECK ((jsonb_typeof(score_reasons) = 'array'::text))CHECK ((trial_duration_days >= 0))
LostReason
Table lost_reason · Resource lost_reasons
GET /api/v1/lost_reasonsGET /api/v1/lost_reasons/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
display_order | integer · int4 | |||
is_active | boolean · bool | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
crm.affiliate_created | An affiliate was added. | id |
crm.affiliate_deactivated | An affiliate was closed. | id, openLeads |
crm.affiliate_report_link_issued | An affiliate was sent a report or referral link. | id, affiliateId, purpose |
crm.affiliate_report_link_revoked | An affiliate report link was closed. | id, affiliateId |
crm.affiliate_updated | An affiliate was edited. | id |
crm.call_amended | A recorded call was corrected. | id, leadId |
crm.call_dictionary_changed | The call vocabulary changed. | id, table |
crm.call_logged | A call was recorded. | id, leadId |
crm.call_template_changed | A call template changed. | id |
crm.calls_exported | Somebody exported the call log as one sheet per caller. | count |
crm.lead_archived | A lead was archived. | id |
crm.lead_assigned | A lead changed owner. | id, userId |
crm.lead_attributed | A lead was credited to an affiliate, or the credit moved. | id, affiliateId, previousAffiliateId |
crm.lead_cancelled | A customer withdrew after buying. | id, customerId |
crm.lead_captured | A lead was opened. | id, customerId, affiliateId |
crm.lead_lost | A lead was lost, by hand or by the auto-close rule. | id, customerId, lostReasonId, automatic |
crm.lead_restored | An archived lead came back. | id |
crm.lead_source_changed | The source vocabulary changed. | id |
crm.lead_stage_changed | A lead moved through the pipeline. | id, stageId, behavior, stageKey, fromStageId, fromStageKey, customerId |
crm.lead_stage_definition_changed | The pipeline changed. | id |
crm.lead_stale | An open lead has not moved for a while. | id, customerId, customerName, ownerUserId, stage, quietDays, lastMovedAt |
crm.lead_trial_started | A trial began. | id, days |
crm.lead_updated | A lead was edited. | id |
crm.lead_won | A lead was won — finance opens the agreement. | id, customerId |
crm.leads_exported | Somebody took the lead list out as a file. | count, view, truncated |
crm.leads_scored | Open leads were given a score. | count |
crm.lost_reason_changed | The loss-reason vocabulary changed. | id |