Packages API
Every Packages action (7) and data model (3): endpoints, scopes, input, preconditions, events and examples.
Monthly packages that bundle services and classes, with sessions counted.
Module code packages · 7 actions · 3 data models · 6 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: packages (everything), packages:read (read only) or packages:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
package_items | package_item | /api/v1/package_items | |||
package_usages | package_usage | /api/v1/package_usages | |||
packages | package | /api/v1/packages | packages.create_package | packages.update_package |
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 |
|---|---|---|
packages.create_package | packages:manage | Create a package |
packages.redeem | packages:write | Use a package session |
packages.release | packages:write | Give a session back |
packages.retire_package | packages:manage | Take off sale |
packages.set_items | packages:manage | Change what it covers |
packages.update_package | packages:manage | Edit a package |
packages.use_on_my_booking | packages:write | Use my package |
packages.create_package
Create a package — A period, a price and what it covers.
Needs packages:manage · changes data
https://app.getera.io/api/v1/packages/actions/create_package- REST alias:
POST /api/v1/packages - Canonical address:
POST /api/v1/actions/packages.create_package
Input
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Name · max 255 |
periodMonths | number | yes | Months · min 1 · max 120 |
priceAmount | number (money) | yes | Price (minor units) · min 1 |
priceCurrency | string | Currency · max 3 | |
autoRenew | boolean | Renews by default | |
items | object / array | yes | What it covers · [{ itemVariantId? | groupClassId?, sessionsPerPeriod? (empty = unlimited) }] |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_items | At least one item, each either a service or a class. |
service_not_found | Every service must exist. |
class_not_found | Every class must exist and still run. |
Events it publishes: packages.package_created
What it writes: package (insert) — Creates the package; package_item (insert) — Writes what it covers
curl -X POST https://app.getera.io/api/v1/packages/actions/create_package \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name","periodMonths":1,"priceAmount":1,"items":{}}'{
"ok": true,
"result": "…",
"events": [
"packages.package_created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.packages.create({
name: "Name",
periodMonths: 1,
priceAmount: 1,
items: {}
})era packages create --data '{"name":"Name","periodMonths":1,"priceAmount":1,"items":{}}'{
"name": "era__packages__create_package",
"arguments": {
"name": "Name",
"periodMonths": 1,
"priceAmount": 1,
"items": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold packages:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_items, service_not_found, class_not_found · 429 slow down
packages.redeem
Use a package session — Covers a booking from a subscription, while it has sessions left.
Needs packages:write · changes data
https://app.getera.io/api/v1/package_usages/actions/redeem- Canonical address:
POST /api/v1/actions/packages.redeem
Input
| Field | Type | Required | Notes |
|---|---|---|---|
subscriptionId | uuid | yes | Subscription · id of subscription |
recordType | string (enum) | yes | Booking kind · Allowed values: appointment_line, class_joining |
recordId | uuid | yes | Booking |
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 | The subscription must be active. |
record_not_found | The booking must exist and not be cancelled. |
customer_mismatch | The booking must be for the subscriber. |
subscription_expired | The booking must fall inside the subscription’s window. |
not_covered | The package must include what was booked. |
already_redeemed | A booking uses at most one session. |
no_sessions_left | The period’s sessions must not all be used. |
Events it publishes: packages.session_used, packages.almost_used_up
What it writes: package_usage (insert) — Records the session drawn
curl -X POST https://app.getera.io/api/v1/package_usages/actions/redeem \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"subscriptionId":"SUBSCRIPTION_ID","recordType":"appointment_line","recordId":"RECORD_ID"}'{
"ok": true,
"result": "…",
"events": [
"packages.session_used",
"packages.almost_used_up"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.packageUsages.redeem({
subscriptionId: "SUBSCRIPTION_ID",
recordType: "appointment_line",
recordId: "RECORD_ID"
})era package_usages redeem \
--subscription-id "SUBSCRIPTION_ID" \
--record-type "appointment_line" \
--record-id "RECORD_ID"{
"name": "era__packages__redeem",
"arguments": {
"subscriptionId": "SUBSCRIPTION_ID",
"recordType": "appointment_line",
"recordId": "RECORD_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold packages:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 subscription_not_found, subscription_not_active, record_not_found, customer_mismatch, subscription_expired, not_covered, already_redeemed, no_sessions_left · 429 slow down
packages.release
Give a session back — Undoes a session drawn by mistake. The booking itself is not touched.
Needs packages:write · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/package_usages/{id}/actions/release- Canonical address:
POST /api/v1/actions/packages.release
Input
| Field | Type | Required | Notes |
|---|---|---|---|
usageId | uuid | yes | Filled from the path {id} · Session · id of package_usage |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
usage_not_found | The session must have been drawn from a package. |
Events it publishes: packages.session_released
What it writes: package_usage (delete) — Removes the usage
curl -X POST https://app.getera.io/api/v1/package_usages/PACKAGE_USAGE_ID/actions/release \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"packages.session_released"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.packageUsages.release("PACKAGE_USAGE_ID")era package_usages release PACKAGE_USAGE_ID{
"name": "era__packages__release",
"arguments": {
"usageId": "PACKAGE_USAGE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold packages:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 usage_not_found · 429 slow down
packages.retire_package
Take off sale — Stops selling it. Running subscriptions carry on to their end dates.
Needs packages:manage · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/packages/{id}/actions/retire_package- Canonical address:
POST /api/v1/actions/packages.retire_package
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Package · id of package |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The package must exist. |
package_retired | It must still be on sale. |
Events it publishes: packages.package_retired
What it writes: package (update) — Stamps archived_at
curl -X POST https://app.getera.io/api/v1/packages/PACKAGE_ID/actions/retire_package \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"packages.package_retired"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.packages.retirePackage("PACKAGE_ID")era packages retire_package PACKAGE_ID{
"name": "era__packages__retire_package",
"arguments": {
"id": "PACKAGE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold packages:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, package_retired · 429 slow down
packages.set_items
Change what it covers — The services and classes in the package, with how many sessions each.
Needs packages:manage · removes or cannot be undone
https://app.getera.io/api/v1/package_items/actions/set_items- Canonical address:
POST /api/v1/actions/packages.set_items
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Package · id of package |
items | object / array | yes | What it covers · [{ itemVariantId? | groupClassId?, sessionsPerPeriod? (empty = unlimited) }] |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The package must exist. |
package_retired | A package off sale cannot be edited. |
invalid_items | At least one item, each either a service or a class. |
service_not_found | Every service must exist. |
class_not_found | Every class must exist and still run. |
items_in_use | Contents cannot change once sessions have been used from them. |
Events it publishes: packages.package_updated
What it writes: package_item (delete) — Clears the old contents; package_item (insert) — Writes the new contents
curl -X POST https://app.getera.io/api/v1/package_items/actions/set_items \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"id":"PACKAGE_ID","items":{}}'{
"ok": true,
"result": "…",
"events": [
"packages.package_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.packageItems.setItems({
id: "PACKAGE_ID",
items: {}
})era package_items set_items --data '{"id":"PACKAGE_ID","items":{}}'{
"name": "era__packages__set_items",
"arguments": {
"id": "PACKAGE_ID",
"items": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold packages:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, package_retired, invalid_items, service_not_found, class_not_found, items_in_use · 429 slow down
packages.update_package
Edit a package — Name, period, price and renewal default — for new sales only.
Needs packages:manage · changes data
https://app.getera.io/api/v1/packages/{id}/actions/update_package- REST alias:
PATCH /api/v1/packages/{id} - Canonical address:
POST /api/v1/actions/packages.update_package
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Package · id of package |
name | string | Name · max 255 | |
periodMonths | number | Months · min 1 · max 120 | |
priceAmount | number (money) | Price (minor units) · min 1 | |
priceCurrency | string | Currency · max 3 | |
autoRenew | boolean | Renews by default |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The package must exist. |
package_retired | A package off sale cannot be edited. |
Events it publishes: packages.package_updated
What it writes: package (update) — Updates the package
curl -X POST https://app.getera.io/api/v1/packages/PACKAGE_ID/actions/update_package \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"packages.package_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.packages.updatePackage("PACKAGE_ID", {
name: "Name"
})era packages update_package PACKAGE_ID \
--name "Name"{
"name": "era__packages__update_package",
"arguments": {
"id": "PACKAGE_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold packages:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, package_retired · 429 slow down
packages.use_on_my_booking
Use my package — A signed-in customer covers the booking they just made from their package, while it has sessions left.
Needs packages: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/package_usages/actions/use_on_my_booking- Canonical address:
POST /api/v1/actions/packages.use_on_my_booking
Input
| Field | Type | Required | Notes |
|---|---|---|---|
subscriptionId | uuid | yes | Package · id of subscription |
appointmentId | uuid | yes | Booking · id of appointment |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_your_booking | Only a booking the customer is a guest on. |
no_sessions_left | The period’s sessions must not all be used. |
Events it publishes: packages.session_used, packages.almost_used_up
What it writes: package_usage (insert) — Records the session drawn
Errors: 401 no or expired credential · 403 the credential does not hold packages:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_your_booking, no_sessions_left · 429 slow down
Data models
PackageItem
Table package_item · Resource package_items
GET /api/v1/package_itemsGET /api/v1/package_items/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
package_id | string (uuid) · uuid | package | ||
item_variant_id | string (uuid) · uuid | yes | item_variant | |
group_class_id | string (uuid) · uuid | yes | group_class | |
sessions_per_period | integer · int4 | yes | ||
sort_order | integer · int4 | |||
created_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((num_nonnulls(item_variant_id, group_class_id) = 1))CHECK (((sessions_per_period IS NULL) OR (sessions_per_period > 0)))
PackageUsage
Table package_usage · Resource package_usages
GET /api/v1/package_usagesGET /api/v1/package_usages/{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 | ||
package_item_id | string (uuid) · uuid | package_item | ||
record_type | string · text | appointment_line, class_joining | ||
record_id | string (uuid) · uuid | |||
used_on | string (date) · date | |||
created_by | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz |
Primary key: id
Unique: record_type, record_id
Package
Table package · Resource packages
GET /api/v1/packagesGET /api/v1/packages/{id}POST /api/v1/packages→packages.create_packagePATCH /api/v1/packages/{id}→packages.update_package
| 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 | |||
period_months | integer · int4 | |||
price_amount | string (int64) · int8 | |||
price_currency | string · bpchar | yes | max 3 | |
auto_renew | boolean · bool | |||
archived_at | string (date-time) · timestamptz | yes | ||
created_by | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
is_public | boolean · bool | yes |
Primary key: id
Checks:
CHECK (((length(btrim(name)) >= 1) AND (length(btrim(name)) <= 255)))CHECK ((period_months > 0))CHECK ((price_amount >= 0))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
packages.almost_used_up | A subscription has two or fewer sessions left this period — time to offer the renewal. | id, subscriptionId, packageId, packageName, packageItemId, customerId, remaining, included, endsOn, autoRenew |
packages.package_created | A package was put on sale. | id, name, periodMonths, priceAmount |
packages.package_retired | A package was taken off sale. | id |
packages.package_updated | A package changed. | id |
packages.session_released | A package session was given back. | id, subscriptionId |
packages.session_used | A booking was covered by a package. | id, subscriptionId, recordType, recordId, customerId, remaining |