Subscriptions API
Every Subscriptions action (9) and data model (2): endpoints, scopes, input, preconditions, events and examples.
Recurring plans at a fixed price, renewed and billed every period.
Module code subscriptions · 9 actions · 2 data models · 9 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: subscriptions (everything), subscriptions:read (read only) or subscriptions:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
subscription_periods | subscription_period | /api/v1/subscription_periods | |||
subscriptions | subscription | /api/v1/subscriptions |
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 |
|---|---|---|
subscriptions.buy_package_online | subscriptions:write | Buy a package online |
subscriptions.cancel | subscriptions:write | Cancel |
subscriptions.pause | subscriptions:write | Pause |
subscriptions.raise_ending_notices | subscriptions:write | Raise “ending soon” notices |
subscriptions.renew | subscriptions:write | Renew now |
subscriptions.renew_due | subscriptions:manage | Renew everything due |
subscriptions.resume | subscriptions:write | Resume |
subscriptions.set_auto_renew | subscriptions:write | Renewal |
subscriptions.start | subscriptions:write | Start a subscription |
subscriptions.buy_package_online
Buy a package online — A signed-in customer starts a package from the site: a subscription at the package’s price, its first period open from today.
Needs subscriptions:write · changes data · Also callable from a published site (customer)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/subscriptions/actions/buy_package_online- Canonical address:
POST /api/v1/actions/subscriptions.buy_package_online
Input
| Field | Type | Required | Notes |
|---|---|---|---|
packageId | uuid | yes | Package · id of package |
autoRenew | boolean | Renews automatically |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
package_not_found | The package must be on sale. |
subscription_already_active | One live subscription per person per package. |
Events it publishes: subscriptions.started, subscriptions.period_started
What it writes: subscription (insert) — Starts the subscription; subscription_period (insert) — Opens the first period
Errors: 401 no or expired credential · 403 the credential does not hold subscriptions:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 package_not_found, subscription_already_active · 429 slow down
subscriptions.cancel
Cancel — Ends it now, or at the end of the period already paid for.
Needs subscriptions:write · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/subscriptions/{id}/actions/cancel- Canonical address:
POST /api/v1/actions/subscriptions.cancel
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Subscription · id of subscription |
atPeriodEnd | boolean | At the end of this period | |
reason | string | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
subscription_not_found | The subscription must exist. |
subscription_already_ended | An ended subscription is not cancelled again. |
Events it publishes: subscriptions.cancelled
What it writes: subscription (update) — Cancels it; subscription_period (update) — Cancels a period that has not started
curl -X POST https://app.getera.io/api/v1/subscriptions/SUBSCRIPTION_ID/actions/cancel \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"atPeriodEnd":true}'{
"ok": true,
"result": "…",
"events": [
"subscriptions.cancelled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.subscriptions.cancel("SUBSCRIPTION_ID", {
atPeriodEnd: true
})era subscriptions cancel SUBSCRIPTION_ID \
--at-period-end true{
"name": "era__subscriptions__cancel",
"arguments": {
"id": "SUBSCRIPTION_ID",
"atPeriodEnd": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold subscriptions:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 subscription_not_found, subscription_already_ended · 429 slow down
subscriptions.pause
Pause — Stops renewals without ending the subscription.
Needs subscriptions:write · changes data
https://app.getera.io/api/v1/subscriptions/{id}/actions/pause- Canonical address:
POST /api/v1/actions/subscriptions.pause
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Subscription · id of subscription |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
subscription_not_found | The subscription must exist. |
subscription_not_active | Only an active subscription pauses. |
Events it publishes: subscriptions.paused
What it writes: subscription (update) — Pauses it
curl -X POST https://app.getera.io/api/v1/subscriptions/SUBSCRIPTION_ID/actions/pause \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"subscriptions.paused"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.subscriptions.pause("SUBSCRIPTION_ID")era subscriptions pause SUBSCRIPTION_ID{
"name": "era__subscriptions__pause",
"arguments": {
"id": "SUBSCRIPTION_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold subscriptions:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 subscription_not_found, subscription_not_active · 429 slow down
subscriptions.raise_ending_notices
Raise “ending soon” notices — Machine-only (the daily routine): every subscription that will not renew and ends within the window is told about once per period.
Needs subscriptions:write · changes data
403 machine_only).https://app.getera.io/api/v1/subscriptions/actions/raise_ending_notices- Canonical address:
POST /api/v1/actions/subscriptions.raise_ending_notices
Input
| Field | Type | Required | Notes |
|---|---|---|---|
asOf | date | As of |
Events it publishes: subscriptions.ending_soon
What it writes: subscription (update) — Records that the ending notice was raised
Errors: 401 no or expired credential · 403 the credential does not hold subscriptions:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 429 slow down
subscriptions.renew
Renew now — Opens the next period — or ends the subscription when its term or its cancellation says so.
Needs subscriptions:write · changes data
https://app.getera.io/api/v1/subscriptions/{id}/actions/renew- Canonical address:
POST /api/v1/actions/subscriptions.renew
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Subscription · id of subscription |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
subscription_not_found | The subscription must exist. |
subscription_not_renewable | Only an active or trialing subscription renews. |
Events it publishes: subscriptions.renewed, subscriptions.period_started, subscriptions.cancelled, subscriptions.expired
What it writes: subscription (update) — Moves it onto the next period; subscription_period (insert) — Opens the period
curl -X POST https://app.getera.io/api/v1/subscriptions/SUBSCRIPTION_ID/actions/renew \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"subscriptions.renewed",
"subscriptions.period_started",
"subscriptions.cancelled",
"subscriptions.expired"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.subscriptions.renew("SUBSCRIPTION_ID")era subscriptions renew SUBSCRIPTION_ID{
"name": "era__subscriptions__renew",
"arguments": {
"id": "SUBSCRIPTION_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold subscriptions:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 subscription_not_found, subscription_not_renewable · 429 slow down
subscriptions.renew_due
Renew everything due — Renews every automatic subscription whose renewal date has come.
Needs subscriptions:manage · changes data
https://app.getera.io/api/v1/subscription_periods/actions/renew_due- Canonical address:
POST /api/v1/actions/subscriptions.renew_due
Input
| Field | Type | Required | Notes |
|---|---|---|---|
asOf | date | As of |
Events it publishes: subscriptions.renewed, subscriptions.period_started, subscriptions.cancelled, subscriptions.expired
What it writes: subscription (update) — Moves each onto its next period; subscription_period (insert) — Opens the periods
curl -X POST https://app.getera.io/api/v1/subscription_periods/actions/renew_due \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"asOf":"2026-11-02"}'{
"ok": true,
"result": "…",
"events": [
"subscriptions.renewed",
"subscriptions.period_started",
"subscriptions.cancelled",
"subscriptions.expired"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.subscriptionPeriods.renewDue({
asOf: "2026-11-02"
})era subscription_periods renew_due \
--as-of "2026-11-02"{
"name": "era__subscriptions__renew_due",
"arguments": {
"asOf": "2026-11-02"
}
}Errors: 401 no or expired credential · 403 the credential does not hold subscriptions:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 429 slow down
subscriptions.resume
Resume — Picks a paused subscription back up; the next renewal is today at the earliest.
Needs subscriptions:write · changes data
https://app.getera.io/api/v1/subscriptions/{id}/actions/resume- Canonical address:
POST /api/v1/actions/subscriptions.resume
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Subscription · id of subscription |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
subscription_not_found | The subscription must exist. |
subscription_not_paused | Only a paused subscription resumes. |
Events it publishes: subscriptions.resumed
What it writes: subscription (update) — Resumes it
curl -X POST https://app.getera.io/api/v1/subscriptions/SUBSCRIPTION_ID/actions/resume \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"subscriptions.resumed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.subscriptions.resume("SUBSCRIPTION_ID")era subscriptions resume SUBSCRIPTION_ID{
"name": "era__subscriptions__resume",
"arguments": {
"id": "SUBSCRIPTION_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold subscriptions:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 subscription_not_found, subscription_not_paused · 429 slow down
subscriptions.set_auto_renew
Renewal — Whether it renews by itself when the period ends; the daily sweep ends one that does not.
Needs subscriptions:write · changes data
https://app.getera.io/api/v1/subscriptions/{id}/actions/set_auto_renew- Canonical address:
POST /api/v1/actions/subscriptions.set_auto_renew
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Subscription · id of subscription |
autoRenew | boolean | yes | Renew automatically |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
subscription_not_found | The subscription must exist. |
subscription_already_ended | An ended subscription does not renew. |
Events it publishes: subscriptions.auto_renew_changed
What it writes: subscription (update) — Sets auto_renew
curl -X POST https://app.getera.io/api/v1/subscriptions/SUBSCRIPTION_ID/actions/set_auto_renew \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"autoRenew":true}'{
"ok": true,
"result": "…",
"events": [
"subscriptions.auto_renew_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.subscriptions.setAutoRenew("SUBSCRIPTION_ID", {
autoRenew: true
})era subscriptions set_auto_renew SUBSCRIPTION_ID \
--auto-renew true{
"name": "era__subscriptions__set_auto_renew",
"arguments": {
"id": "SUBSCRIPTION_ID",
"autoRenew": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold subscriptions:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 subscription_not_found, subscription_already_ended · 429 slow down
subscriptions.start
Start a subscription — A recurring term at a locked price. The first period opens now, or when the trial ends.
Needs subscriptions:write · changes data
https://app.getera.io/api/v1/subscriptions/actions/start- Canonical address:
POST /api/v1/actions/subscriptions.start
Input
| Field | Type | Required | Notes |
|---|---|---|---|
customerId | uuid | yes | Subscriber · id of customer |
name | string | yes | Plan · max 255 |
amount | number (money) | yes | Price per period |
currency | string | Currency · max 3 | |
billingCycle | string (enum) | Every · Allowed values: weekly, monthly, quarterly, yearly | |
startDate | date | Starts | |
trialDays | number | Trial days · min 0 · max 365 | |
endDate | date | Ends | |
autoRenew | boolean | Renews automatically | |
itemModule | string | Item module · max 40 | |
itemRecordId | uuid | Item | |
periodMonths | number | Months per period · A plan sold in whole months (a package); overrides the cycle. · min 1 · max 120 | |
notes | string | Notes · max 4000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
customer_not_found | The person must exist. |
customer_archived | An archived person does not subscribe. |
invalid_currency | The currency must be an ISO-4217 code. |
subscription_amount_not_positive | A subscription costs more than zero. |
subscription_already_active | One live subscription per person per plan. |
subscription_end_before_start | A term cannot end before it starts. |
Events it publishes: subscriptions.started, subscriptions.period_started
What it writes: subscription (insert) — Starts the subscription; subscription_period (insert) — Opens the first period
curl -X POST https://app.getera.io/api/v1/subscriptions/actions/start \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customerId":"CUSTOMER_ID","name":"Plan","amount":1}'{
"ok": true,
"result": "…",
"events": [
"subscriptions.started",
"subscriptions.period_started"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.subscriptions.start({
customerId: "CUSTOMER_ID",
name: "Plan",
amount: 1
})era subscriptions start \
--customer-id "CUSTOMER_ID" \
--name "Plan" \
--amount 1{
"name": "era__subscriptions__start",
"arguments": {
"customerId": "CUSTOMER_ID",
"name": "Plan",
"amount": 1
}
}Errors: 401 no or expired credential · 403 the credential does not hold subscriptions:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 customer_not_found, customer_archived, invalid_currency, subscription_amount_not_positive, subscription_already_active, subscription_end_before_start · 429 slow down
Data models
SubscriptionPeriod
Table subscription_period · Resource subscription_periods
GET /api/v1/subscription_periodsGET /api/v1/subscription_periods/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
subscription_id | string (uuid) · uuid | subscription | ||
sequence | integer · int4 | |||
period_start | string (date) · date | |||
period_end | string (date) · date | |||
currency | string · bpchar | max 3 | ||
amount | string (int64) · int8 | |||
status | string · text | current, completed, cancelled | ||
created_at | string (date-time) · timestamptz |
Primary key: id
Unique: subscription_id, sequence
Checks:
CHECK ((amount >= 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK ((period_end >= period_start))CHECK ((sequence > 0))
Subscription
Table subscription · Resource subscriptions
GET /api/v1/subscriptionsGET /api/v1/subscriptions/{id}
| 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 | ||
name | string · text | |||
item_module | string · text | yes | ||
item_record_id | string (uuid) · uuid | yes | ||
status | string · text | trialing, active, paused, cancelled, expired | ||
billing_cycle | string · text | weekly, monthly, quarterly, yearly | ||
currency | string · bpchar | max 3 | ||
amount | string (int64) · int8 | |||
start_date | string (date) · date | |||
trial_ends_at | string (date) · date | yes | ||
end_date | string (date) · date | yes | ||
current_period_start | string (date) · date | yes | ||
current_period_end | string (date) · date | yes | ||
next_renewal_date | string (date) · date | yes | ||
auto_renew | boolean · bool | |||
cancel_at_period_end | boolean · bool | |||
cancelled_at | string (date-time) · timestamptz | yes | ||
cancel_reason | string · text | yes | ||
paused_at | string (date-time) · timestamptz | yes | ||
notes | string · text | yes | ||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
period_months | integer · int4 | yes | ||
renewals | integer · int4 | |||
ending_notice_at | string (date-time) · timestamptz | yes |
Primary key: id
Checks:
CHECK ((amount >= 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK (((end_date IS NULL) OR (end_date >= start_date)))CHECK ((length(TRIM(BOTH FROM name)) > 0))CHECK (((period_months IS NULL) OR (period_months > 0)))CHECK ((renewals >= 0))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
subscriptions.auto_renew_changed | A subscription’s renewal setting changed. | id, autoRenew |
subscriptions.cancelled | A subscription was cancelled. | id, atPeriodEnd, customerId, itemModule |
subscriptions.ending_soon | A membership that will not renew ends soon. | id, customerId, customerName, name, endsOn, daysLeft, itemModule, itemRecordId |
subscriptions.expired | A subscription ran out — its term ended, or its period ended without renewing. | id, customerId, autoRenew, itemModule, itemRecordId |
subscriptions.paused | A subscription was paused. | id |
subscriptions.period_started | A paid period began — finance opens its agreement. | id, subscriptionId, customerId, name, sequence, periodStart, periodEnd, amount, currency, itemModule, itemRecordId |
subscriptions.renewed | A subscription renewed. | id, count |
subscriptions.resumed | A subscription was resumed. | id |
subscriptions.started | A subscription started — a plan, or a package sold as a membership. | id, customerId, trialEndsAt, name, startsOn, endsOn, itemModule, itemRecordId |