Finance API
Every Finance action (42) and data model (13): endpoints, scopes, input, preconditions, events and examples.
What customers owe and pay, and what the business spends.
Module code finance · 42 actions · 13 data models · 30 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: finance (everything), finance:read (read only) or finance:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
agreement_lines | agreement_line | /api/v1/agreement_lines | |||
agreements | agreement | /api/v1/agreements | finance.create_agreement | finance.update_agreement | |
commission_payouts | commission_payout | /api/v1/commission_payouts | |||
commissions | commission | /api/v1/commissions | |||
discount_lines | discount_line | /api/v1/discount_lines | |||
expense_categories | expense_category | /api/v1/expense_categories | finance.save_expense_category | ||
expenses | expense | /api/v1/expenses | finance.create_expense | finance.update_expense | |
finance_provider_events | finance_provider_event | /api/v1/finance_provider_events | |||
installments | installment | /api/v1/installments | finance.update_installment | ||
payment_allocations | payment_allocation | /api/v1/payment_allocations | |||
payment_events | payment_event | /api/v1/payment_events | |||
payment_provider_configs | payment_provider_config | /api/v1/payment_provider_configs | |||
payments | payment | /api/v1/payments |
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 |
|---|---|---|
finance.accrue_commission | finance:write | Accrue a commission |
finance.allocate_payment | finance:write | Allocate a collection |
finance.apply_discount | finance:write | Apply a discount |
finance.approve_expense | finance:approve | Approve an expense |
finance.approve_refund | finance:approve | Approve a refund |
finance.cancel_agreement | finance:manage | Cancel an agreement |
finance.cancel_commission | finance:manage | Cancel a commission |
finance.cancel_installment | finance:manage | Cancel an instalment |
finance.cancel_payment | finance:write | Release a payment |
finance.capture_payment | finance:write | Capture a payment |
finance.configure_payment_app | finance:manage | Configure a payment app |
finance.create_agreement | finance:write | Open an agreement |
finance.create_expense | finance:write | Record an expense |
finance.deactivate_expense_category | finance:manage | Deactivate an expense category |
finance.mark_expense_paid | finance:write | Mark an expense paid |
finance.open_draft_for_source | finance:write | Draft an agreement for a record |
finance.pay_site_agreement | finance:write | Pay by link (site) |
finance.pay_site_order | finance:write | Pay for an order (site) |
finance.raise_installment_notices | finance:write | Raise instalment reminders |
finance.recalculate_commission | finance:write | Recalculate the commission |
finance.record_commission_payout | finance:approve | Record a commission payout |
finance.record_gift_card_payment | finance:write | Record a gift card collection |
finance.record_payment | finance:write | Record a collection |
finance.record_provider_webhook | finance:write | Record a payment webhook |
finance.record_source_discounts | finance:write | Record the shop’s discounts |
finance.refund_payment | finance:manage | Refund a collection |
finance.reject_expense | finance:approve | Reject an expense |
finance.reject_refund | finance:approve | Reject a refund |
finance.remove_discount | finance:manage | Remove a discount |
finance.request_refund | finance:write | Request a refund |
finance.request_refunds_for_source | finance:write | Answer a cancellation |
finance.return_gift_card_payment | finance:write | Return a gift card collection |
finance.run_recurring_expenses | finance:write | Record recurring expenses |
finance.save_expense_category | finance:manage | Save an expense category |
finance.schedule_installments | finance:write | Schedule instalments |
finance.set_commission_status | finance:manage | Move a commission |
finance.stop_recurring_expense | finance:write | Stop repeating |
finance.take_payment | finance:write | Take a payment |
finance.update_agreement | finance:write | Edit an agreement |
finance.update_expense | finance:write | Edit an expense |
finance.update_installment | finance:write | Edit an instalment |
finance.void_expense | finance:manage | Void an expense |
finance.accrue_commission
Accrue a commission — Opens what the affiliate is owed, at the rate frozen on the day of the agreement.
Needs finance:write · changes data
https://app.getera.io/api/v1/commissions/actions/accrue_commission- Canonical address:
POST /api/v1/actions/finance.accrue_commission
Input
| Field | Type | Required | Notes |
|---|---|---|---|
agreementId | uuid | yes | Agreement · id of agreement |
rate | number | Rate (%) · min 0 · max 100 | |
amount | number (money) | Amount · min 0 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
agreement_not_found | The agreement must exist. |
commission_already_exists | One commission per agreement. |
agreement_has_no_affiliate | The agreement must name an affiliate. |
affiliate_not_active | The affiliate must be active. |
Events it publishes: finance.commission_accrued
What it writes: commission (insert) — Opens the commission
curl -X POST https://app.getera.io/api/v1/commissions/actions/accrue_commission \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"agreementId":"AGREEMENT_ID"}'{
"ok": true,
"result": "…",
"events": [
"finance.commission_accrued"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.commissions.accrueCommission({
agreementId: "AGREEMENT_ID"
})era commissions accrue_commission \
--agreement-id "AGREEMENT_ID"{
"name": "era__finance__accrue_commission",
"arguments": {
"agreementId": "AGREEMENT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 agreement_not_found, commission_already_exists, agreement_has_no_affiliate, affiliate_not_active · 429 slow down
finance.allocate_payment
Allocate a collection — Spreads one collection across the instalments it paid off.
Needs finance:write · changes data
https://app.getera.io/api/v1/payment_allocations/actions/allocate_payment- Canonical address:
POST /api/v1/actions/finance.allocate_payment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
paymentId | uuid | yes | Collection · id of payment |
allocations | object / array | yes | Allocations |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
payment_not_found | The collection must exist. |
no_allocations | At least one allocation is needed. |
unknown_allocation_target | The target must be a known kind of record. |
invalid_allocation_amount | Every allocation must be greater than zero. |
duplicate_allocation_target | A target appears only once in one allocation request. |
allocation_exceeds_payment | Allocations may not add up to more than was collected. |
allocation_target_not_found | The target must exist and remain available. |
allocation_currency_mismatch | The collection and target must use the same currency. |
allocation_customer_mismatch | A customer collection cannot settle somebody else’s balance. |
allocation_exceeds_target | An allocation cannot exceed the target’s outstanding balance. |
Events it publishes: finance.payment_allocated
What it writes: payment_allocation (insert) — Writes the allocations
curl -X POST https://app.getera.io/api/v1/payment_allocations/actions/allocate_payment \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"paymentId":"PAYMENT_ID","allocations":{}}'{
"ok": true,
"result": "…",
"events": [
"finance.payment_allocated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.paymentAllocations.allocatePayment({
paymentId: "PAYMENT_ID",
allocations: {}
})era payment_allocations allocate_payment --data '{"paymentId":"PAYMENT_ID","allocations":{}}'{
"name": "era__finance__allocate_payment",
"arguments": {
"paymentId": "PAYMENT_ID",
"allocations": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 payment_not_found, no_allocations, unknown_allocation_target, invalid_allocation_amount, duplicate_allocation_target, allocation_exceeds_payment, allocation_target_not_found, allocation_currency_mismatch, allocation_customer_mismatch, allocation_exceeds_target · 429 slow down
finance.apply_discount
Apply a discount — Reduces an agreement, and freezes the reduced amount on the row.
Needs finance:write · changes data
https://app.getera.io/api/v1/agreements/{id}/actions/apply_discount- Canonical address:
POST /api/v1/actions/finance.apply_discount
Input
| Field | Type | Required | Notes |
|---|---|---|---|
agreementId | uuid | yes | Filled from the path {id} · Agreement · id of agreement |
valueType | string (enum) | Kind · Allowed values: percentage, fixed | |
value | number | yes | Value |
name | string | Label · max 255 | |
reason | string | yes | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
agreement_not_found | The agreement must exist. |
agreement_closed | A closed agreement is not discounted. |
discount_value_negative | A discount is not negative. |
discount_percentage_over_100 | A percentage discount stops at 100. |
discount_after_collection | A discount may not take the agreement below what has been collected. |
discount_over_cap | The workspace discount cap must not be exceeded. |
discount_actor_required | A manual discount records who applied it. |
Events it publishes: finance.discount_applied
What it writes: discount_line (insert) — Writes the discount; agreement (update) — Freezes the discounted total
curl -X POST https://app.getera.io/api/v1/agreements/AGREEMENT_ID/actions/apply_discount \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"value":1,"reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"finance.discount_applied"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.agreements.applyDiscount("AGREEMENT_ID", {
value: 1,
reason: "Reason"
})era agreements apply_discount AGREEMENT_ID \
--value 1 \
--reason "Reason"{
"name": "era__finance__apply_discount",
"arguments": {
"agreementId": "AGREEMENT_ID",
"value": 1,
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 agreement_not_found, agreement_closed, discount_value_negative, discount_percentage_over_100, discount_after_collection, discount_over_cap, discount_actor_required · 429 slow down
finance.approve_expense
Approve an expense — Signs an expense off. Never your own.
Needs finance:approve · changes data
https://app.getera.io/api/v1/expenses/{id}/actions/approve_expense- Canonical address:
POST /api/v1/actions/finance.approve_expense
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Expense · id of expense |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_not_found | The expense must exist. |
expense_already_decided | The expense must still be pending. |
cannot_approve_own_expense | Nobody approves their own spending. |
Events it publishes: finance.expense_approved
What it writes: expense (update) — Stamps the approval
curl -X POST https://app.getera.io/api/v1/expenses/EXPENSE_ID/actions/approve_expense \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_approved"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenses.approveExpense("EXPENSE_ID")era expenses approve_expense EXPENSE_ID{
"name": "era__finance__approve_expense",
"arguments": {
"id": "EXPENSE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:approve · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_not_found, expense_already_decided, cannot_approve_own_expense · 429 slow down
finance.approve_refund
Approve a refund — Approves a request and executes it: the money is recorded as gone back.
Needs finance:approve · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/payment_events/{id}/actions/approve_refund- Canonical address:
POST /api/v1/actions/finance.approve_refund
Input
| Field | Type | Required | Notes |
|---|---|---|---|
requestId | uuid | yes | Filled from the path {id} · Request · id of payment_event |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
refund_request_not_found | The request must exist. |
refund_request_resolved | A request is approved or rejected once. |
refund_not_available | The provider must still allow a refund. |
payment_app_not_connected | The payment app must still be enabled. |
payment_credentials_invalid | The payment app credentials must open from the vault. |
Events it publishes: finance.refund_approved, finance.payment_refunded
What it writes: payment_event (insert) — Records the refund step; payment (update) — Moves the pending and refunded amounts; payment_allocation (insert) — Writes the negative allocation; installment (update) — Reopens an instalment it no longer covers
curl -X POST https://app.getera.io/api/v1/payment_events/PAYMENT_EVENT_ID/actions/approve_refund \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"finance.refund_approved",
"finance.payment_refunded"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.paymentEvents.approveRefund("PAYMENT_EVENT_ID")era payment_events approve_refund PAYMENT_EVENT_ID{
"name": "era__finance__approve_refund",
"arguments": {
"requestId": "PAYMENT_EVENT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:approve · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 refund_request_not_found, refund_request_resolved, refund_not_available, payment_app_not_connected, payment_credentials_invalid · 429 slow down
finance.cancel_agreement
Cancel an agreement — Closes the agreement and its pending instalments. Nothing is deleted.
Needs finance:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/agreements/{id}/actions/cancel_agreement- Canonical address:
POST /api/v1/actions/finance.cancel_agreement
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Agreement · id of agreement |
reason | string | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
agreement_not_found | The agreement must exist. |
already_cancelled | An agreement is cancelled once. |
agreement_has_collection | Money already collected must be refunded before cancelling. |
Events it publishes: finance.agreement_cancelled
What it writes: agreement (update) — Marks it cancelled; installment (update) — Cancels the pending instalments
curl -X POST https://app.getera.io/api/v1/agreements/AGREEMENT_ID/actions/cancel_agreement \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"finance.agreement_cancelled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.agreements.cancelAgreement("AGREEMENT_ID", {
reason: "Reason"
})era agreements cancel_agreement AGREEMENT_ID \
--reason "Reason"{
"name": "era__finance__cancel_agreement",
"arguments": {
"id": "AGREEMENT_ID",
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 agreement_not_found, already_cancelled, agreement_has_collection · 429 slow down
finance.cancel_commission
Cancel a commission — Zeroes a commission that has not been paid. A paid one is never removed.
Needs finance:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/commissions/{id}/actions/cancel_commission- Canonical address:
POST /api/v1/actions/finance.cancel_commission
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Commission · id of commission |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
commission_not_found | The commission must exist. |
paid_commission_not_deletable | A paid commission is reversed, never removed. |
Events it publishes: finance.commission_cancelled
What it writes: commission (update) — Cancels it and zeroes the amount
curl -X POST https://app.getera.io/api/v1/commissions/COMMISSION_ID/actions/cancel_commission \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"finance.commission_cancelled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.commissions.cancelCommission("COMMISSION_ID")era commissions cancel_commission COMMISSION_ID{
"name": "era__finance__cancel_commission",
"arguments": {
"id": "COMMISSION_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 commission_not_found, paid_commission_not_deletable · 429 slow down
finance.cancel_installment
Cancel an instalment — Takes an instalment out of the plan without deleting it.
Needs finance:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/installments/{id}/actions/cancel_installment- Canonical address:
POST /api/v1/actions/finance.cancel_installment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Instalment · id of installment |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
installment_not_found | The instalment must exist. |
installment_has_collection | An instalment with money against it is refunded, not cancelled. |
Events it publishes: finance.installment_cancelled
What it writes: installment (update) — Marks it cancelled
curl -X POST https://app.getera.io/api/v1/installments/INSTALLMENT_ID/actions/cancel_installment \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"finance.installment_cancelled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.installments.cancelInstallment("INSTALLMENT_ID")era installments cancel_installment INSTALLMENT_ID{
"name": "era__finance__cancel_installment",
"arguments": {
"id": "INSTALLMENT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 installment_not_found, installment_has_collection · 429 slow down
finance.cancel_payment
Release a payment — Cancels what is authorized and not charged.
Needs finance:write · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/payments/{id}/actions/cancel_payment- Canonical address:
POST /api/v1/actions/finance.cancel_payment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
paymentId | uuid | yes | Filled from the path {id} · Payment · id of payment |
outcome | string (enum) | Test outcome · Only the dummy gateway reads this. · Allowed values: success, failure, pending |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
payment_not_found | The payment must exist. |
cancel_not_available | The provider must still allow a release. |
payment_app_not_connected | The payment app must still be enabled. |
payment_credentials_invalid | The payment app credentials must open from the vault. |
Events it publishes: finance.payment_provider_event
What it writes: payment (update) — Moves the amounts and stores what the provider now allows; payment_event (insert) — Records the provider’s answer
curl -X POST https://app.getera.io/api/v1/payments/PAYMENT_ID/actions/cancel_payment \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"outcome":"success"}'{
"ok": true,
"result": "…",
"events": [
"finance.payment_provider_event"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.payments.cancelPayment("PAYMENT_ID", {
outcome: "success"
})era payments cancel_payment PAYMENT_ID \
--outcome "success"{
"name": "era__finance__cancel_payment",
"arguments": {
"paymentId": "PAYMENT_ID",
"outcome": "success"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 payment_not_found, cancel_not_available, payment_app_not_connected, payment_credentials_invalid · 429 slow down
finance.capture_payment
Capture a payment — Charges money that was only authorized.
Needs finance:write · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/payments/{id}/actions/capture_payment- Canonical address:
POST /api/v1/actions/finance.capture_payment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
paymentId | uuid | yes | Filled from the path {id} · Payment · id of payment |
amount | number (money) | Amount | |
outcome | string (enum) | Test outcome · Only the dummy gateway reads this. · Allowed values: success, failure, pending |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
payment_not_found | The payment must exist. |
charge_not_available | The provider must still allow a capture. |
capture_exceeds_authorized | A capture takes at most what is held. |
payment_app_not_connected | The payment app must still be enabled. |
payment_credentials_invalid | The payment app credentials must open from the vault. |
Events it publishes: finance.payment_provider_event, finance.payment_recorded
What it writes: payment (update) — Moves the amounts and stores what the provider now allows; payment_event (insert) — Records the provider’s answer; payment_allocation (insert) — Puts charged money on the instalment; installment (update) — Closes the instalment once covered
curl -X POST https://app.getera.io/api/v1/payments/PAYMENT_ID/actions/capture_payment \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"amount":1}'{
"ok": true,
"result": "…",
"events": [
"finance.payment_provider_event",
"finance.payment_recorded"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.payments.capturePayment("PAYMENT_ID", {
amount: 1
})era payments capture_payment PAYMENT_ID \
--amount 1{
"name": "era__finance__capture_payment",
"arguments": {
"paymentId": "PAYMENT_ID",
"amount": 1
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 payment_not_found, charge_not_available, capture_exceeds_authorized, payment_app_not_connected, payment_credentials_invalid · 429 slow down
finance.configure_payment_app
Configure a payment app — Enables, orders and selects test mode for a connected payment app.
Needs finance:manage · changes data
https://app.getera.io/api/v1/payment_provider_configs/actions/configure_payment_app- Canonical address:
POST /api/v1/actions/finance.configure_payment_app
Input
| Field | Type | Required | Notes |
|---|---|---|---|
provider | string | yes | Provider · max 40 |
credentialId | uuid | yes | Connected app · id of credential |
enabled | boolean | Enabled | |
testMode | boolean | Test mode | |
position | number | Order · min 0 | |
displayName | string | Checkout label · max 120 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
unsupported_payment_app | Only a known online payment app can be configured. |
payment_credential_mismatch | The connected app must belong to the selected provider. |
payment_app_unavailable | The connected app must be active. |
Events it publishes: finance.payment_method_changed
What it writes: payment_provider_config (upsert) — Saves checkout enablement, order and test mode
curl -X POST https://app.getera.io/api/v1/payment_provider_configs/actions/configure_payment_app \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"provider":"Provider","credentialId":"CREDENTIAL_ID"}'{
"ok": true,
"result": "…",
"events": [
"finance.payment_method_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.paymentProviderConfigs.configurePaymentApp({
provider: "Provider",
credentialId: "CREDENTIAL_ID"
})era payment_provider_configs configure_payment_app \
--provider "Provider" \
--credential-id "CREDENTIAL_ID"{
"name": "era__finance__configure_payment_app",
"arguments": {
"provider": "Provider",
"credentialId": "CREDENTIAL_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 unsupported_payment_app, payment_credential_mismatch, payment_app_unavailable · 429 slow down
finance.create_agreement
Open an agreement — Records what somebody owes, in the workspace currency unless told otherwise.
Needs finance:write · changes data
https://app.getera.io/api/v1/agreements/actions/create_agreement- REST alias:
POST /api/v1/agreements - Canonical address:
POST /api/v1/actions/finance.create_agreement
Input
| Field | Type | Required | Notes |
|---|---|---|---|
customerId | uuid | yes | Person · id of customer |
kind | string (enum) | Kind · Allowed values: contract, order, booking, subscription_term | |
description | string | What it is for · max 4000 | |
note | string | Internal note · max 4000 | |
currency | string | Currency · Prefilled from the workspace default. Never typed from scratch. · max 3 | |
totalAmount | number (money) | Subtotal · min 0 | |
taxTotalAmount | number (money) | Tax · min 0 | |
lines | object / array | Lines | |
sourceModule | string | Came from · max 40 | |
sourceRecordId | uuid | Source record | |
affiliateId | uuid | Referred by · id of affiliate |
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 take on an agreement. |
invalid_currency | The currency must be an ISO-4217 code. |
affiliate_not_active | The referring affiliate must be active. |
agreement_total_not_positive | An agreement must be worth more than zero. |
Events it publishes: finance.agreement_created
What it writes: agreement (insert) — Opens the agreement; agreement_line (insert) — Writes its lines
curl -X POST https://app.getera.io/api/v1/agreements/actions/create_agreement \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customerId":"CUSTOMER_ID"}'{
"ok": true,
"result": "…",
"events": [
"finance.agreement_created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.agreements.create({
customerId: "CUSTOMER_ID"
})era agreements create \
--customer-id "CUSTOMER_ID"{
"name": "era__finance__create_agreement",
"arguments": {
"customerId": "CUSTOMER_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 customer_not_found, customer_archived, invalid_currency, affiliate_not_active, agreement_total_not_positive · 429 slow down
finance.create_expense
Record an expense — Money going out, against a category, awaiting approval.
Needs finance:write · changes data
https://app.getera.io/api/v1/expenses/actions/create_expense- REST alias:
POST /api/v1/expenses - Canonical address:
POST /api/v1/actions/finance.create_expense
Input
| Field | Type | Required | Notes |
|---|---|---|---|
categoryId | uuid | yes | Category · id of expense_category |
title | string | yes | What it was · max 255 |
description | string | Description · max 4000 | |
amount | number (money) | yes | Amount |
currency | string | Currency · max 3 | |
expenseDate | date | yes | Date |
method | string (enum) | Paid by · Allowed values: transfer, card, cash, other | |
vendorName | string | Supplier · max 255 | |
vendorContact | string | Supplier contact · max 255 | |
receiptFileId | uuid | Receipt · id of file | |
isRecurring | boolean | Recurring | |
recurringPeriod | string (enum) | Every · Allowed values: weekly, monthly, quarterly, yearly | |
nextOccurrence | date | Next due | |
recurringUntil | date | Repeat until |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_category_not_available | The category must exist and be active. |
invalid_currency | The currency must be an ISO-4217 code. |
expense_amount_negative | An expense is not negative. |
expense_amount_over_cap | The workspace expense limit applies. |
expense_date_in_future | An expense date cannot be in the future. |
expense_recurring_period_required | A recurring expense needs a period. |
Events it publishes: finance.expense_created
What it writes: expense (insert) — Records the expense
curl -X POST https://app.getera.io/api/v1/expenses/actions/create_expense \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"categoryId":"EXPENSE_CATEGORY_ID","title":"What it was","amount":1,"expenseDate":"2026-11-02"}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenses.create({
categoryId: "EXPENSE_CATEGORY_ID",
title: "What it was",
amount: 1,
expenseDate: "2026-11-02"
})era expenses create \
--category-id "EXPENSE_CATEGORY_ID" \
--title "What it was" \
--amount 1 \
--expense-date "2026-11-02"{
"name": "era__finance__create_expense",
"arguments": {
"categoryId": "EXPENSE_CATEGORY_ID",
"title": "What it was",
"amount": 1,
"expenseDate": "2026-11-02"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_category_not_available, invalid_currency, expense_amount_negative, expense_amount_over_cap, expense_date_in_future, expense_recurring_period_required · 429 slow down
finance.deactivate_expense_category
Deactivate an expense category — Stops new expenses landing on it. Existing ones keep theirs.
Needs finance:manage · changes data
https://app.getera.io/api/v1/expense_categories/{id}/actions/deactivate_expense_category- Canonical address:
POST /api/v1/actions/finance.deactivate_expense_category
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Category · id of expense_category |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_category_not_found | The category must exist. |
Events it publishes: finance.expense_category_changed
What it writes: expense_category (update) — Clears is_active
curl -X POST https://app.getera.io/api/v1/expense_categories/EXPENSE_CATEGORY_ID/actions/deactivate_expense_category \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_category_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenseCategories.deactivateExpenseCategory("EXPENSE_CATEGORY_ID")era expense_categories deactivate_expense_category EXPENSE_CATEGORY_ID{
"name": "era__finance__deactivate_expense_category",
"arguments": {
"id": "EXPENSE_CATEGORY_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_category_not_found · 429 slow down
finance.mark_expense_paid
Mark an expense paid — Stamps the day the money left. The date is what "paid" means here.
Needs finance:write · changes data
https://app.getera.io/api/v1/expenses/{id}/actions/mark_expense_paid- Canonical address:
POST /api/v1/actions/finance.mark_expense_paid
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Expense · id of expense |
paymentDate | date | Paid on |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_not_found | The expense must exist. |
expense_not_approved | An expense is approved before it is paid. |
expense_paid_without_date | Marking an expense paid needs the date. |
Events it publishes: finance.expense_paid
What it writes: expense (update) — Stamps the payment date
curl -X POST https://app.getera.io/api/v1/expenses/EXPENSE_ID/actions/mark_expense_paid \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"paymentDate":"2026-11-02"}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_paid"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenses.markExpensePaid("EXPENSE_ID", {
paymentDate: "2026-11-02"
})era expenses mark_expense_paid EXPENSE_ID \
--payment-date "2026-11-02"{
"name": "era__finance__mark_expense_paid",
"arguments": {
"id": "EXPENSE_ID",
"paymentDate": "2026-11-02"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_not_found, expense_not_approved, expense_paid_without_date · 429 slow down
finance.open_draft_for_source
Draft an agreement for a record — Machine-only: the agreement a won lead, a booking, a package, an order or a subscription period implies.
Needs finance:write · changes data
403 machine_only).https://app.getera.io/api/v1/agreements/actions/open_draft_for_source- Canonical address:
POST /api/v1/actions/finance.open_draft_for_source
Input
| Field | Type | Required | Notes |
|---|---|---|---|
sourceModule | string | yes | Came from · max 40 |
sourceRecordId | uuid | yes | Source record |
customerId | uuid | yes | Person · id of customer |
kind | string (enum) | Kind · Allowed values: contract, order, booking, subscription_term | |
totalAmount | number (money) | Total · min 0 | |
currency | string | Currency · max 3 | |
description | string | What it is for · max 4000 | |
activate | boolean | Open as active | |
lines | object / array | What was sold | |
taxTotalAmount | number (money) | Tax on top · min 0 | |
discounts | object / array | Discounts applied at the source | |
affiliateId | uuid | Came through affiliate · id of affiliate | |
giftCardPayments | object / array | Paid by gift cards |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
agreement_exists_for_source | One open agreement per source record. |
customer_not_found | The person must exist. |
customer_archived | An archived person takes on nothing. |
invalid_currency | The currency must be an ISO-4217 code. |
payment_amount_not_positive | A gift card collection is more than zero. |
allocation_exceeds_target | Gift cards cannot pay more than the agreement is worth. |
Events it publishes: finance.agreement_created, finance.payment_recorded
What it writes: agreement (insert) — Drafts the agreement; agreement (update) — Totals the discounts it came with; agreement_line (insert) — Writes what was sold, with its frozen tax, when the source says; discount_line (insert) — Writes the promotions and coupons the source applied; payment (insert) — Records what gift cards already paid, as collections; payment_event (insert) — The gift card charge; payment_allocation (insert) — Allocates it to the agreement
Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 agreement_exists_for_source, customer_not_found, customer_archived, invalid_currency, payment_amount_not_positive, allocation_exceeds_target · 429 slow down
finance.pay_site_agreement
Pay by link (site) — A visitor pays what a booking deposit, ticket, package or open instalment still owes, by its payment link, through a connected payment app or the test method.
Needs finance:write · reaches outside (sends, publishes or charges) · Also callable from a published site (visitor)
/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/installments/actions/pay_site_agreement- Canonical address:
POST /api/v1/actions/finance.pay_site_agreement
Input
| Field | Type | Required | Notes |
|---|---|---|---|
token | uuid | yes | Payment link |
provider | string | Payment method · Not needed when a gift card on the checkout pays it all. · max 40 | |
returnUrl | string | Return URL · max 2000 | |
cancelUrl | string | Cancel URL · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
pay_link_invalid | The link names an open agreement of this business. |
payment_method_unavailable | The method is a connected payment app, or the test method outside production. |
gift_card_hold_expired | A gift card put on the checkout is still held (fifteen minutes) when it pays. |
nothing_due | Something is still owed on the link. |
Events it publishes: finance.payment_provider_event, finance.payment_recorded, finance.installments_scheduled
What it writes: installment (insert) — The whole agreement as one instalment due today, when it has none; payment (insert) — Opens the transaction; payment_event (insert) — Records the provider’s answer; payment_allocation (insert) — Puts what was charged, and what a gift card paid, on the instalment; installment (update) — Closes the instalment when card and method cover it
Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 pay_link_invalid, payment_method_unavailable, gift_card_hold_expired, nothing_due · 429 slow down
finance.pay_site_order
Pay for an order (site) — A visitor pays what their website order still owes, through a connected payment app or the test method.
Needs finance:write · reaches outside (sends, publishes or charges) · Also callable from a published site (visitor)
/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/installments/actions/pay_site_order- Canonical address:
POST /api/v1/actions/finance.pay_site_order
Input
| Field | Type | Required | Notes |
|---|---|---|---|
token | uuid | yes | Basket |
provider | string | Payment method · Not needed when a gift card on the checkout pays it all. · max 40 | |
returnUrl | string | Return URL · max 2000 | |
cancelUrl | string | Cancel URL · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
order_not_found | The basket token names a placed order. |
payment_method_unavailable | The method is a connected payment app, or the test method outside production. |
gift_card_hold_expired | A gift card put on the checkout is still held (fifteen minutes) when it pays. |
payment_not_ready | Finance has opened the order’s agreement. |
Events it publishes: finance.payment_provider_event, finance.payment_recorded, finance.installments_scheduled
What it writes: installment (insert) — The whole order as one instalment due today, when it has none; payment (insert) — Opens the transaction; payment_event (insert) — Records the provider’s answer; payment_allocation (insert) — Puts what was charged, and what a gift card paid, on the instalment; installment (update) — Closes the instalment when card and method cover it
Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 order_not_found, payment_method_unavailable, gift_card_hold_expired, payment_not_ready · 429 slow down
finance.raise_installment_notices
Raise instalment reminders — Machine-only (the daily finance routine): flags every pending instalment falling due within the reminder window, and every one past due, once each.
Needs finance:write · changes data
403 machine_only).https://app.getera.io/api/v1/installments/actions/raise_installment_notices- Canonical address:
POST /api/v1/actions/finance.raise_installment_notices
Input
| Field | Type | Required | Notes |
|---|---|---|---|
asOf | date | As of |
Events it publishes: finance.installment_due, finance.installment_overdue
What it writes: installment (update) — Records that the customer was told (due soon / overdue)
Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 429 slow down
finance.recalculate_commission
Recalculate the commission — K-09: brings a pending commission in line with what has been collected (calculate_commission_amount_v3). Runs on every collection and refund; the button runs it by hand.
Needs finance:write · changes data
https://app.getera.io/api/v1/commissions/actions/recalculate_commission- Canonical address:
POST /api/v1/actions/finance.recalculate_commission
Input
| Field | Type | Required | Notes |
|---|---|---|---|
agreementId | uuid | Agreement · id of agreement | |
paymentId | uuid | Collection · id of payment |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
commission_not_found | There must be a commission to recalculate. |
Events it publishes: finance.commission_recalculated
What it writes: commission (update) — Writes the amount the collections support
curl -X POST https://app.getera.io/api/v1/commissions/actions/recalculate_commission \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"agreementId":"AGREEMENT_ID"}'{
"ok": true,
"result": "…",
"events": [
"finance.commission_recalculated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.commissions.recalculateCommission({
agreementId: "AGREEMENT_ID"
})era commissions recalculate_commission \
--agreement-id "AGREEMENT_ID"{
"name": "era__finance__recalculate_commission",
"arguments": {
"agreementId": "AGREEMENT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 commission_not_found · 429 slow down
finance.record_commission_payout
Record a commission payout — One transfer to an affiliate that pays a set of pending commissions.
Needs finance:approve · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/commission_payouts/actions/record_commission_payout- Canonical address:
POST /api/v1/actions/finance.record_commission_payout
Input
| Field | Type | Required | Notes |
|---|---|---|---|
affiliateId | uuid | yes | Affiliate · id of affiliate |
commissionIds | object / array | yes | Commissions |
paidOn | date | Paid on | |
method | string (enum) | Method · Allowed values: transfer, cash, card, other | |
reference | string | Transfer reference · max 255 | |
note | string | Note · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_commissions_selected | A payout pays at least one commission. |
affiliate_not_found | The affiliate must exist. |
commission_not_payable | Each commission is pending, unpaid, this affiliate’s, one currency. |
Events it publishes: finance.commission_payout_recorded, finance.commission_status_changed
What it writes: commission_payout (insert) — Records the transfer; commission (update) — Marks the commissions paid by it
curl -X POST https://app.getera.io/api/v1/commission_payouts/actions/record_commission_payout \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"affiliateId":"AFFILIATE_ID","commissionIds":{}}'{
"ok": true,
"result": "…",
"events": [
"finance.commission_payout_recorded",
"finance.commission_status_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.commissionPayouts.recordCommissionPayout({
affiliateId: "AFFILIATE_ID",
commissionIds: {}
})era commission_payouts record_commission_payout --data '{"affiliateId":"AFFILIATE_ID","commissionIds":{}}'{
"name": "era__finance__record_commission_payout",
"arguments": {
"affiliateId": "AFFILIATE_ID",
"commissionIds": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:approve · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_commissions_selected, affiliate_not_found, commission_not_payable · 429 slow down
finance.record_gift_card_payment
Record a gift card collection — Machine-only (gift_cards.redeemed): the part of a sale a gift card paid, as a collection against its agreement.
Needs finance:write · changes data
403 machine_only).https://app.getera.io/api/v1/payments/actions/record_gift_card_payment- Canonical address:
POST /api/v1/actions/finance.record_gift_card_payment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
sourceModule | string | yes | Came from · max 40 |
sourceRecordId | uuid | yes | Source record |
giftCardId | uuid | yes | Gift card |
giftCardEventId | uuid | yes | Redemption |
amount | number (money) | yes | Amount |
currency | string | yes | Currency · max 3 |
codeTail | string | Last characters of the code · max 8 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_agreement_for_source | The sale must already be in finance. |
allocation_currency_mismatch | The card pays only in the agreement currency. |
payment_amount_not_positive | A collection is more than zero. |
allocation_exceeds_target | A card cannot pay more than is left to collect. |
Events it publishes: finance.payment_recorded
What it writes: payment (insert) — The gift card collection; payment_event (insert) — Its charge; payment_allocation (insert) — Allocated to the agreement
Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_agreement_for_source, allocation_currency_mismatch, payment_amount_not_positive, allocation_exceeds_target · 429 slow down
finance.record_payment
Record a collection — Money arrived: writes the movement, the event and what it paid off.
Needs finance:write · changes data
https://app.getera.io/api/v1/installments/{id}/actions/record_payment- Canonical address:
POST /api/v1/actions/finance.record_payment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
installmentId | uuid | yes | Filled from the path {id} · Instalment · id of installment |
amount | number (money) | yes | Amount |
method | string (enum) | Method · Allowed values: transfer, card, cash, other | |
methodName | string | Method detail · max 255 | |
reference | string | Provider reference · max 512 | |
idempotencyKey | string | Idempotency key · Scoped to this workspace and provider — never global. · max 512 | |
occurredAt | date-time | When it happened | |
note | string | Note · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
installment_not_found | The instalment must exist. |
installment_cancelled | Nothing is collected against a cancelled instalment. |
payment_amount_not_positive | A collection is more than zero. |
duplicate_payment_idempotency_key | The same collection is never recorded twice. |
overpayment | Allocations may not exceed the instalment. |
Events it publishes: finance.payment_recorded
What it writes: payment (insert) — Records the movement; payment_event (insert) — Records the charge event; payment_allocation (insert) — Says what it paid off; installment (update) — Closes the instalment when it is covered
curl -X POST https://app.getera.io/api/v1/installments/INSTALLMENT_ID/actions/record_payment \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"amount":1}'{
"ok": true,
"result": "…",
"events": [
"finance.payment_recorded"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.installments.recordPayment("INSTALLMENT_ID", {
amount: 1
})era installments record_payment INSTALLMENT_ID \
--amount 1{
"name": "era__finance__record_payment",
"arguments": {
"installmentId": "INSTALLMENT_ID",
"amount": 1
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 installment_not_found, installment_cancelled, payment_amount_not_positive, duplicate_payment_idempotency_key, overpayment · 429 slow down
finance.record_provider_webhook
Record a payment webhook — Admits one verified provider event once and applies it to the canonical payment.
Needs finance:write · reaches outside (sends, publishes or charges)
https://app.getera.io/api/v1/payments/{id}/actions/record_provider_webhook- Canonical address:
POST /api/v1/actions/finance.record_provider_webhook
Input
| Field | Type | Required | Notes |
|---|---|---|---|
provider | string | yes | Provider · max 40 |
externalId | string | yes | Provider event · max 512 |
paymentId | uuid | Filled from the path {id} · Payment · id of payment | |
pspReference | string | Provider reference · max 512 | |
type | string | yes | Event type · max 80 |
amount | number (money) | yes | Amount · min 0 |
currency | string | yes | Currency · max 3 |
message | string | Message · max 2000 | |
occurredAt | date-time | yes | Occurred at |
payloadDigest | string | yes | Payload digest · max 128 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
unknown_payment_provider | The provider must be known. |
webhook_payment_not_found | The event must name a payment in this workspace. |
webhook_provider_mismatch | The event and payment providers must match. |
Events it publishes: finance.payment_provider_event, finance.payment_recorded
What it writes: finance_provider_event (insert) — Admits the external event once; payment (update) — Moves the amounts and stores what the provider now allows; payment_event (insert) — Records the provider’s answer; payment_allocation (insert) — Puts charged money on the instalment; installment (update) — Closes the instalment once covered
curl -X POST https://app.getera.io/api/v1/payments/PAYMENT_ID/actions/record_provider_webhook \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"provider":"Provider","externalId":"Provider event","type":"Event type","amount":1,"currency":"Currency","occurredAt":"2026-11-02T09:00:00Z"}'{
"ok": true,
"result": "…",
"events": [
"finance.payment_provider_event",
"finance.payment_recorded"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.payments.recordProviderWebhook("PAYMENT_ID", {
provider: "Provider",
externalId: "Provider event",
type: "Event type",
amount: 1,
currency: "Currency",
occurredAt: "2026-11-02T09:00:00Z"
})era payments record_provider_webhook PAYMENT_ID \
--provider "Provider" \
--external-id "Provider event" \
--type "Event type" \
--amount 1 \
--currency "Currency" \
--occurred-at "2026-11-02T09:00:00Z"{
"name": "era__finance__record_provider_webhook",
"arguments": {
"paymentId": "PAYMENT_ID",
"provider": "Provider",
"externalId": "Provider event",
"type": "Event type",
"amount": 1,
"currency": "Currency",
"occurredAt": "2026-11-02T09:00:00Z"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 unknown_payment_provider, webhook_payment_not_found, webhook_provider_mismatch · 429 slow down
finance.record_source_discounts
Record the shop’s discounts — Machine-only (retail.order_discounts_applied): mirrors the promotions and coupons on an order as discount lines on its agreement.
Needs finance:write · removes or cannot be undone
403 machine_only).https://app.getera.io/api/v1/discount_lines/actions/record_source_discounts- Canonical address:
POST /api/v1/actions/finance.record_source_discounts
Input
| Field | Type | Required | Notes |
|---|---|---|---|
sourceModule | string | yes | Came from · max 40 |
sourceRecordId | uuid | yes | Source record |
discounts | object / array | Discounts |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_agreement_for_source | The order must be in finance already. |
What it writes: discount_line (delete) — Drops what the shop wrote before; discount_line (insert) — Writes the current set; agreement (update) — Totals the discounts
Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_agreement_for_source · 429 slow down
finance.refund_payment
Refund a collection — Sends money back. A refund is an event on the collection, not a record of its own.
Needs finance:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/payments/{id}/actions/refund_payment- Canonical address:
POST /api/v1/actions/finance.refund_payment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
paymentId | uuid | yes | Filled from the path {id} · Collection · id of payment |
amount | number (money) | yes | Amount |
reason | string | yes | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
payment_not_found | The collection must exist. |
refund_not_available | The provider must still allow a refund on this collection. |
payment_app_not_connected | The payment app must still be enabled. |
payment_credentials_invalid | The payment app credentials must open from the vault. |
refund_exceeds_charged | A refund cannot exceed what was charged. |
Events it publishes: finance.payment_refunded
What it writes: payment_event (insert) — Records the refund event; payment (update) — Raises the refunded amount; payment_allocation (insert) — Writes the negative allocation; installment (update) — Reopens an instalment it no longer covers
curl -X POST https://app.getera.io/api/v1/payments/PAYMENT_ID/actions/refund_payment \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"amount":1,"reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"finance.payment_refunded"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.payments.refundPayment("PAYMENT_ID", {
amount: 1,
reason: "Reason"
})era payments refund_payment PAYMENT_ID \
--amount 1 \
--reason "Reason"{
"name": "era__finance__refund_payment",
"arguments": {
"paymentId": "PAYMENT_ID",
"amount": 1,
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 payment_not_found, refund_not_available, payment_app_not_connected, payment_credentials_invalid, refund_exceeds_charged · 429 slow down
finance.reject_expense
Reject an expense — Turns an expense down, with the reason the source never required.
Needs finance:approve · changes data
https://app.getera.io/api/v1/expenses/{id}/actions/reject_expense- Canonical address:
POST /api/v1/actions/finance.reject_expense
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Expense · id of expense |
reason | string | yes | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_not_found | The expense must exist. |
expense_already_decided | The expense must still be pending. |
expense_rejection_reason_required | A rejection needs a reason. |
Events it publishes: finance.expense_rejected
What it writes: expense (update) — Records the rejection
curl -X POST https://app.getera.io/api/v1/expenses/EXPENSE_ID/actions/reject_expense \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_rejected"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenses.rejectExpense("EXPENSE_ID", {
reason: "Reason"
})era expenses reject_expense EXPENSE_ID \
--reason "Reason"{
"name": "era__finance__reject_expense",
"arguments": {
"id": "EXPENSE_ID",
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:approve · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_not_found, expense_already_decided, expense_rejection_reason_required · 429 slow down
finance.reject_refund
Reject a refund — Turns a request down, with the reason on the collection’s trail.
Needs finance:approve · removes or cannot be undone
https://app.getera.io/api/v1/payment_events/{id}/actions/reject_refund- Canonical address:
POST /api/v1/actions/finance.reject_refund
Input
| Field | Type | Required | Notes |
|---|---|---|---|
requestId | uuid | yes | Filled from the path {id} · Request · id of payment_event |
reason | string | yes | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
refund_request_not_found | The request must exist. |
refund_request_resolved | A request is approved or rejected once. |
Events it publishes: finance.refund_rejected
What it writes: payment_event (insert) — Records the refund step; payment (update) — Moves the pending and refunded amounts
curl -X POST https://app.getera.io/api/v1/payment_events/PAYMENT_EVENT_ID/actions/reject_refund \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"finance.refund_rejected"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.paymentEvents.rejectRefund("PAYMENT_EVENT_ID", {
reason: "Reason"
})era payment_events reject_refund PAYMENT_EVENT_ID \
--reason "Reason"{
"name": "era__finance__reject_refund",
"arguments": {
"requestId": "PAYMENT_EVENT_ID",
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:approve · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 refund_request_not_found, refund_request_resolved · 429 slow down
finance.remove_discount
Remove a discount — Takes a discount off and puts the amount back on the agreement.
Needs finance:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/discount_lines/{id}/actions/remove_discount- Canonical address:
POST /api/v1/actions/finance.remove_discount
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Discount · id of discount_line |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
discount_not_found | The discount must exist. |
agreement_not_found | Its agreement must exist. |
agreement_closed | A closed agreement keeps its discounts. |
Events it publishes: finance.discount_removed
What it writes: discount_line (delete) — Removes the discount; agreement (update) — Restores the total
curl -X POST https://app.getera.io/api/v1/discount_lines/DISCOUNT_LINE_ID/actions/remove_discount \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"finance.discount_removed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.discountLines.removeDiscount("DISCOUNT_LINE_ID")era discount_lines remove_discount DISCOUNT_LINE_ID{
"name": "era__finance__remove_discount",
"arguments": {
"id": "DISCOUNT_LINE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 discount_not_found, agreement_not_found, agreement_closed · 429 slow down
finance.request_refund
Request a refund — Asks for money to go back. Nothing moves until somebody who can approve says yes.
Needs finance:write · removes or cannot be undone
https://app.getera.io/api/v1/payments/{id}/actions/request_refund- Canonical address:
POST /api/v1/actions/finance.request_refund
Input
| Field | Type | Required | Notes |
|---|---|---|---|
paymentId | uuid | yes | Filled from the path {id} · Collection · id of payment |
amount | number (money) | yes | Amount |
reason | string | yes | Reason · max 2000 |
idempotencyKey | string | Idempotency key · max 512 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
payment_not_found | The collection must exist. |
refund_not_available | The provider must still allow a refund. |
refund_exceeds_charged | A refund cannot exceed what is still refundable. |
Events it publishes: finance.refund_requested
What it writes: payment_event (insert) — Records the refund step; payment (update) — Moves the pending and refunded amounts
curl -X POST https://app.getera.io/api/v1/payments/PAYMENT_ID/actions/request_refund \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"amount":1,"reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"finance.refund_requested"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.payments.requestRefund("PAYMENT_ID", {
amount: 1,
reason: "Reason"
})era payments request_refund PAYMENT_ID \
--amount 1 \
--reason "Reason"{
"name": "era__finance__request_refund",
"arguments": {
"paymentId": "PAYMENT_ID",
"amount": 1,
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 payment_not_found, refund_not_available, refund_exceeds_charged · 429 slow down
finance.request_refunds_for_source
Answer a cancellation — Machine-only: cancels what was not collected and raises refund requests for what was, when a booking, lead or order is cancelled.
Needs finance:write · changes data
403 machine_only).https://app.getera.io/api/v1/payment_events/actions/request_refunds_for_source- Canonical address:
POST /api/v1/actions/finance.request_refunds_for_source
Input
| Field | Type | Required | Notes |
|---|---|---|---|
sourceModule | string | yes | Came from · max 40 |
sourceRecordId | uuid | yes | Source record |
reason | string | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_agreement_for_source | Something in finance must hang off the record. |
Events it publishes: finance.agreement_cancelled, finance.refund_requested
What it writes: agreement (update) — Cancels an uncollected agreement; installment (update) — Cancels its pending instalments; payment_event (insert) — Records the refund step; payment (update) — Moves the pending and refunded amounts
Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_agreement_for_source · 429 slow down
finance.return_gift_card_payment
Return a gift card collection — Machine-only (gift_cards.refunded): money that went back onto the card is taken off its collection, so it is owed again.
Needs finance:write · changes data
403 machine_only).https://app.getera.io/api/v1/payment_events/actions/return_gift_card_payment- Canonical address:
POST /api/v1/actions/finance.return_gift_card_payment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
sourceModule | string | yes | Came from · max 40 |
sourceRecordId | uuid | yes | Source record |
giftCardId | uuid | yes | Gift card |
amount | number (money) | yes | Amount |
idempotencyKey | string | Idempotency key · max 512 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_agreement_for_source | The sale must be in finance. |
gift_card_payment_not_found | That card must have paid against this sale. |
refund_exceeds_charged | No more goes back than the card paid. |
Events it publishes: finance.payment_refunded
What it writes: payment_event (insert) — The return, as a refund; payment (update) — Raises the refunded amount; payment_allocation (insert) — The negative allocation that reopens the balance; installment (update) — Reopens the instalment a site-checkout card had closed
Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_agreement_for_source, gift_card_payment_not_found, refund_exceeds_charged · 429 slow down
finance.run_recurring_expenses
Record recurring expenses — Copies every recurring expense that has come due into a new expense awaiting approval.
Needs finance:write · changes data
https://app.getera.io/api/v1/expenses/actions/run_recurring_expenses- Canonical address:
POST /api/v1/actions/finance.run_recurring_expenses
Input
| Field | Type | Required | Notes |
|---|---|---|---|
asOf | date | As of |
Events it publishes: finance.expense_created
What it writes: expense (insert) — One pending copy per period that came due; expense (update) — Moves the template’s next occurrence on
curl -X POST https://app.getera.io/api/v1/expenses/actions/run_recurring_expenses \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"asOf":"2026-11-02"}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenses.runRecurringExpenses({
asOf: "2026-11-02"
})era expenses run_recurring_expenses \
--as-of "2026-11-02"{
"name": "era__finance__run_recurring_expenses",
"arguments": {
"asOf": "2026-11-02"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 429 slow down
finance.save_expense_category
Save an expense category — Adds or renames an expense category for this workspace.
Needs finance:manage · changes data
https://app.getera.io/api/v1/expense_categories/{id}/actions/save_expense_category- REST alias:
PATCH /api/v1/expense_categories/{id} - Canonical address:
POST /api/v1/actions/finance.save_expense_category
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Category · id of expense_category | |
name | string | yes | Name · max 255 |
description | string | Description · max 2000 | |
colour | string | Colour · max 7 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_category_name_taken | Category names are unique per workspace. |
expense_category_not_found | The category being edited must exist. |
Events it publishes: finance.expense_category_changed
What it writes: expense_category (upsert) — Saves the category
curl -X POST https://app.getera.io/api/v1/expense_categories/EXPENSE_CATEGORY_ID/actions/save_expense_category \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_category_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenseCategories.saveExpenseCategory("EXPENSE_CATEGORY_ID", {
name: "Name"
})era expense_categories save_expense_category EXPENSE_CATEGORY_ID \
--name "Name"{
"name": "era__finance__save_expense_category",
"arguments": {
"id": "EXPENSE_CATEGORY_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_category_name_taken, expense_category_not_found · 429 slow down
finance.schedule_installments
Schedule instalments — Writes the whole due-date plan in one transaction.
Needs finance:write · changes data
https://app.getera.io/api/v1/installments/actions/schedule_installments- Canonical address:
POST /api/v1/actions/finance.schedule_installments
Input
| Field | Type | Required | Notes |
|---|---|---|---|
agreementId | uuid | yes | Agreement · id of agreement |
installments | object / array | yes | Instalments |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
agreement_not_found | The agreement must exist. |
agreement_closed | A closed agreement is not rescheduled. |
no_installments | A schedule needs at least one instalment. |
invalid_installments | Each instalment needs a due date and an amount. |
duplicate_installment_due_date | Two instalments may not fall on the same day. |
installments_do_not_sum_to_total | The instalments must add up to the agreement. |
Events it publishes: finance.installments_scheduled
What it writes: installment (insert) — Writes the schedule
curl -X POST https://app.getera.io/api/v1/installments/actions/schedule_installments \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"agreementId":"AGREEMENT_ID","installments":{}}'{
"ok": true,
"result": "…",
"events": [
"finance.installments_scheduled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.installments.scheduleInstallments({
agreementId: "AGREEMENT_ID",
installments: {}
})era installments schedule_installments --data '{"agreementId":"AGREEMENT_ID","installments":{}}'{
"name": "era__finance__schedule_installments",
"arguments": {
"agreementId": "AGREEMENT_ID",
"installments": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 agreement_not_found, agreement_closed, no_installments, invalid_installments, duplicate_installment_due_date, installments_do_not_sum_to_total · 429 slow down
finance.set_commission_status
Move a commission — Follows the source transition table; a cancelled commission is final.
Needs finance:manage · changes data
https://app.getera.io/api/v1/commissions/{id}/actions/set_commission_status- Canonical address:
POST /api/v1/actions/finance.set_commission_status
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Commission · id of commission |
status | string (enum) | yes | Status · Allowed values: completed, cancelled |
paidAt | date | Paid on |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
commission_not_found | The commission must exist. |
commission_cancelled_is_final | A cancelled commission is not reopened. |
invalid_commission_transition | The transition must be a permitted one. |
Events it publishes: finance.commission_status_changed
What it writes: commission (update) — Moves the commission
curl -X POST https://app.getera.io/api/v1/commissions/COMMISSION_ID/actions/set_commission_status \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"status":"completed"}'{
"ok": true,
"result": "…",
"events": [
"finance.commission_status_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.commissions.setCommissionStatus("COMMISSION_ID", {
status: "completed"
})era commissions set_commission_status COMMISSION_ID \
--status "completed"{
"name": "era__finance__set_commission_status",
"arguments": {
"id": "COMMISSION_ID",
"status": "completed"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 commission_not_found, commission_cancelled_is_final, invalid_commission_transition · 429 slow down
finance.stop_recurring_expense
Stop repeating — The expense stays; it is no longer copied each period.
Needs finance:write · changes data
https://app.getera.io/api/v1/expenses/{id}/actions/stop_recurring_expense- Canonical address:
POST /api/v1/actions/finance.stop_recurring_expense
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Expense · id of expense |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_not_found | The expense must exist. |
expense_not_recurring | Only a recurring expense can stop. |
Events it publishes: finance.expense_updated
What it writes: expense (update) — Clears the recurrence
curl -X POST https://app.getera.io/api/v1/expenses/EXPENSE_ID/actions/stop_recurring_expense \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenses.stopRecurringExpense("EXPENSE_ID")era expenses stop_recurring_expense EXPENSE_ID{
"name": "era__finance__stop_recurring_expense",
"arguments": {
"id": "EXPENSE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_not_found, expense_not_recurring · 429 slow down
finance.take_payment
Take a payment — Authorizes or charges an instalment through a payment provider and records its answer.
Needs finance:write · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/installments/{id}/actions/take_payment- Canonical address:
POST /api/v1/actions/finance.take_payment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
installmentId | uuid | yes | Filled from the path {id} · Instalment · id of installment |
amount | number (money) | yes | Amount |
provider | string | Provider · One of: manual, dummy, stripe, iyzico, paytr. · max 40 | |
mode | string (enum) | Mode · Allowed values: charge, authorize | |
outcome | string (enum) | Test outcome · Only the dummy gateway reads this. · Allowed values: success, failure, pending | |
method | string (enum) | Method · Allowed values: card, transfer, cash, other | |
idempotencyKey | string | Idempotency key · max 255 | |
note | string | Note · max 2000 | |
returnUrl | string | Return URL · max 2000 | |
cancelUrl | string | Cancel URL · max 2000 | |
customerEmail | string | Customer email · max 320 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
unknown_payment_provider | The provider must be installed. |
installment_not_found | The instalment must exist. |
installment_cancelled | Nothing is taken against a cancelled instalment. |
payment_amount_not_positive | A payment is more than zero. |
overpayment | A payment does not exceed what is still owed. |
duplicate_payment_idempotency_key | The same key is never charged twice. |
payment_app_not_connected | An online provider must be an enabled connected app. |
payment_credentials_invalid | The payment app credentials must open from the vault. |
Events it publishes: finance.payment_provider_event, finance.payment_recorded
What it writes: payment (insert) — Opens the transaction; payment (update) — Moves the amounts and stores what the provider now allows; payment_event (insert) — Records the provider’s answer; payment_allocation (insert) — Puts charged money on the instalment; installment (update) — Closes the instalment once covered
curl -X POST https://app.getera.io/api/v1/installments/INSTALLMENT_ID/actions/take_payment \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"amount":1}'{
"ok": true,
"result": "…",
"events": [
"finance.payment_provider_event",
"finance.payment_recorded"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.installments.takePayment("INSTALLMENT_ID", {
amount: 1
})era installments take_payment INSTALLMENT_ID \
--amount 1{
"name": "era__finance__take_payment",
"arguments": {
"installmentId": "INSTALLMENT_ID",
"amount": 1
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 unknown_payment_provider, installment_not_found, installment_cancelled, payment_amount_not_positive, overpayment, duplicate_payment_idempotency_key, payment_app_not_connected, payment_credentials_invalid · 429 slow down
finance.update_agreement
Edit an agreement — Changes what the agreement says — never its currency.
Needs finance:write · changes data
https://app.getera.io/api/v1/agreements/{id}/actions/update_agreement- REST alias:
PATCH /api/v1/agreements/{id} - Canonical address:
POST /api/v1/actions/finance.update_agreement
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Agreement · id of agreement |
status | string (enum) | Status · Allowed values: draft, active, fulfilled | |
description | string | What it is for · max 4000 | |
note | string | Internal note · max 4000 | |
totalAmount | number (money) | Subtotal · min 0 | |
taxTotalAmount | number (money) | Tax · min 0 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
agreement_not_found | The agreement must exist. |
agreement_closed | A fulfilled or cancelled agreement is not edited. |
agreement_has_schedule | The total cannot move under instalments that are already scheduled. |
Events it publishes: finance.agreement_updated
What it writes: agreement (update) — Updates the agreement
curl -X POST https://app.getera.io/api/v1/agreements/AGREEMENT_ID/actions/update_agreement \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"status":"draft"}'{
"ok": true,
"result": "…",
"events": [
"finance.agreement_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.agreements.updateAgreement("AGREEMENT_ID", {
status: "draft"
})era agreements update_agreement AGREEMENT_ID \
--status "draft"{
"name": "era__finance__update_agreement",
"arguments": {
"id": "AGREEMENT_ID",
"status": "draft"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 agreement_not_found, agreement_closed, agreement_has_schedule · 429 slow down
finance.update_expense
Edit an expense — Corrects an expense that has not been approved yet.
Needs finance:write · changes data
https://app.getera.io/api/v1/expenses/{id}/actions/update_expense- REST alias:
PATCH /api/v1/expenses/{id} - Canonical address:
POST /api/v1/actions/finance.update_expense
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Expense · id of expense |
title | string | What it was · max 255 | |
description | string | Description · max 4000 | |
amount | number (money) | Amount · min 0 | |
vendorName | string | Supplier · max 255 | |
vendorContact | string | Supplier contact · max 255 | |
receiptFileId | uuid | Receipt · id of file | |
categoryId | uuid | Category · id of expense_category | |
expenseDate | date | Date | |
method | string (enum) | Paid by · Allowed values: transfer, card, cash, other |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_not_found | The expense must exist. |
expense_already_decided | An approved expense is not edited. |
expense_category_not_available | A new category must exist and be in use. |
Events it publishes: finance.expense_updated
What it writes: expense (update) — Updates the expense
curl -X POST https://app.getera.io/api/v1/expenses/EXPENSE_ID/actions/update_expense \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"What it was"}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenses.updateExpense("EXPENSE_ID", {
title: "What it was"
})era expenses update_expense EXPENSE_ID \
--title "What it was"{
"name": "era__finance__update_expense",
"arguments": {
"id": "EXPENSE_ID",
"title": "What it was"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_not_found, expense_already_decided, expense_category_not_available · 429 slow down
finance.update_installment
Edit an instalment — Moves a due date or changes an amount, never below what has been collected.
Needs finance:write · changes data
https://app.getera.io/api/v1/installments/{id}/actions/update_installment- REST alias:
PATCH /api/v1/installments/{id} - Canonical address:
POST /api/v1/actions/finance.update_installment
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Instalment · id of installment |
dueDate | date | Due date | |
amount | number (money) | Amount · min 0 | |
note | string | Note · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
installment_not_found | The instalment must exist. |
installment_cancelled | A cancelled instalment is not edited. |
installment_below_collected | An instalment cannot be worth less than what has been collected against it. |
Events it publishes: finance.installment_updated
What it writes: installment (update) — Updates the instalment
curl -X POST https://app.getera.io/api/v1/installments/INSTALLMENT_ID/actions/update_installment \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"dueDate":"2026-11-02"}'{
"ok": true,
"result": "…",
"events": [
"finance.installment_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.installments.updateInstallment("INSTALLMENT_ID", {
dueDate: "2026-11-02"
})era installments update_installment INSTALLMENT_ID \
--due-date "2026-11-02"{
"name": "era__finance__update_installment",
"arguments": {
"id": "INSTALLMENT_ID",
"dueDate": "2026-11-02"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 installment_not_found, installment_cancelled, installment_below_collected · 429 slow down
finance.void_expense
Void an expense — Withdraws an unpaid expense. A paid one is corrected, not removed.
Needs finance:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/expenses/{id}/actions/void_expense- Canonical address:
POST /api/v1/actions/finance.void_expense
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Expense · id of expense |
reason | string | yes | Reason · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_not_found | The expense must exist. |
paid_expense_not_voidable | A paid expense is corrected, not voided. |
Events it publishes: finance.expense_voided
What it writes: expense (update) — Records the withdrawal
curl -X POST https://app.getera.io/api/v1/expenses/EXPENSE_ID/actions/void_expense \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"finance.expense_voided"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.expenses.voidExpense("EXPENSE_ID", {
reason: "Reason"
})era expenses void_expense EXPENSE_ID \
--reason "Reason"{
"name": "era__finance__void_expense",
"arguments": {
"id": "EXPENSE_ID",
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold finance:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_not_found, paid_expense_not_voidable · 429 slow down
Data models
AgreementLine
Table agreement_line · Resource agreement_lines
GET /api/v1/agreement_linesGET /api/v1/agreement_lines/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
agreement_id | string (uuid) · uuid | agreement | ||
item_module | string · text | yes | ||
item_record_id | string (uuid) · uuid | yes | ||
name | string · text | |||
quantity | string (decimal) · numeric | |||
currency | string · bpchar | max 3 | ||
unit_amount | string (int64) · int8 | |||
total_amount | string (int64) · int8 | |||
display_order | integer · int4 | |||
created_at | string (date-time) · timestamptz | |||
tax_snapshot | boolean · bool | |||
tax_class_id | string (uuid) · uuid | yes | ||
tax_class_name | string · text | yes | ||
tax_rate | string (decimal) · numeric | |||
prices_entered_with_tax | boolean · bool | |||
discount_amount | string (int64) · int8 | |||
net_amount | string (int64) · int8 | yes | ||
tax_amount | string (int64) · int8 | yes | ||
gross_amount | string (int64) · int8 | yes |
Primary key: id
Checks:
CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK ((quantity > (0)::numeric))CHECK (((discount_amount >= 0) AND (COALESCE(net_amount, (0)::bigint) >= 0) AND (COALESCE(tax_amount, (0)::bigint) >= 0) AND (COALESCE(gross_amount, (0)::bigint) >= 0) AND ((NOT tax_snapshot) OR ((net_amount IS NOT NULL) AND (tax_amount IS NOT NULL) AND (gross_amount IS NOT NULL)))))CHECK (((tax_rate >= (0)::numeric) AND (tax_rate <= (100)::numeric)))CHECK ((total_amount >= 0))CHECK ((unit_amount >= 0))
Agreement
Table agreement · Resource agreements
GET /api/v1/agreementsGET /api/v1/agreements/{id}POST /api/v1/agreements→finance.create_agreementPATCH /api/v1/agreements/{id}→finance.update_agreement
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
kind | string · text | contract, order, booking, subscription_term | ||
status | string · text | draft, active, fulfilled, cancelled | ||
customer_id | string (uuid) · uuid | customer | ||
source_module | string · text | yes | ||
source_record_id | string (uuid) · uuid | yes | ||
affiliate_id | string (uuid) · uuid | yes | affiliate | |
currency | string · bpchar | max 3 | ||
subtotal_amount | string (int64) · int8 | |||
discount_total_amount | string (int64) · int8 | |||
tax_total_amount | string (int64) · int8 | |||
total_amount | string (int64) · int8 | |||
note | string · text | yes | ||
description | 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 | |||
cancelled_at | string (date-time) · timestamptz | yes |
Primary key: id
Checks:
CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK ((discount_total_amount >= 0))CHECK ((subtotal_amount >= 0))CHECK ((tax_total_amount >= 0))CHECK ((total_amount >= 0))
CommissionPayout
Table commission_payout · Resource commission_payouts
GET /api/v1/commission_payoutsGET /api/v1/commission_payouts/{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 | ||
currency | string · bpchar | max 3 | ||
amount | string (int64) · int8 | |||
commission_count | integer · int4 | |||
paid_on | string (date) · date | |||
method | string · text | transfer, cash, card, other | ||
reference | string · text | yes | ||
note | string · text | yes | ||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((amount > 0))CHECK ((commission_count > 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))
Commission
Table commission · Resource commissions
GET /api/v1/commissionsGET /api/v1/commissions/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
agreement_id | string (uuid) · uuid | agreement | ||
affiliate_id | string (uuid) · uuid | affiliate | ||
currency | string · bpchar | max 3 | ||
amount | string (int64) · int8 | |||
rate | string (decimal) · numeric | |||
status | string · text | pending, completed, cancelled | ||
paid_at | string (date) · date | yes | ||
notes | string · text | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
payout_id | string (uuid) · uuid | yes | commission_payout | |
is_manual | boolean · bool | |||
recalculated_at | string (date-time) · timestamptz | yes |
Primary key: id
Checks:
CHECK ((amount >= 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK (((rate >= (0)::numeric) AND (rate <= (100)::numeric)))
DiscountLine
Table discount_line · Resource discount_lines
GET /api/v1/discount_linesGET /api/v1/discount_lines/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
target_type | string · text | agreement, agreement_line, checkout, checkout_line, invoice | ||
target_id | string (uuid) · uuid | |||
type | string · text | manual, promotion, voucher, shipping, order_promotion | ||
value_type | string · text | percentage, fixed | ||
value | string (decimal) · numeric | |||
currency | string · bpchar | max 3 | ||
amount | string (int64) · int8 | |||
name | string · text | yes | ||
reason | string · text | yes | ||
applied_by | string (uuid) · uuid | yes | app_user | |
promotion_rule_id | string (uuid) · uuid | yes | ||
voucher_id | string (uuid) · uuid | yes | ||
voucher_code | string · text | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
fixed_amount | string (int64) · int8 | yes |
Primary key: id
Checks:
CHECK (((value_type <> 'fixed'::text) OR (fixed_amount IS NOT NULL)))CHECK ((amount >= 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK (((fixed_amount IS NULL) OR (fixed_amount >= 0)))CHECK ((value >= (0)::numeric))CHECK (((type <> 'manual'::text) OR (applied_by IS NOT NULL)))CHECK (((value_type <> 'percentage'::text) OR (value <= (100)::numeric)))
ExpenseCategory
Table expense_category · Resource expense_categories
GET /api/v1/expense_categoriesGET /api/v1/expense_categories/{id}PATCH /api/v1/expense_categories/{id}→finance.save_expense_category
| 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 | |||
description | string · text | yes | ||
colour | string · text | |||
is_active | boolean · bool | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Expense
Table expense · Resource expenses
GET /api/v1/expensesGET /api/v1/expenses/{id}POST /api/v1/expenses→finance.create_expensePATCH /api/v1/expenses/{id}→finance.update_expense
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
category_id | string (uuid) · uuid | expense_category | ||
title | string · text | |||
description | string · text | yes | ||
currency | string · bpchar | max 3 | ||
amount | string (int64) · int8 | |||
expense_date | string (date) · date | |||
payment_date | string (date) · date | yes | ||
method | string · text | card, transfer, cash, other | ||
vendor_name | string · text | yes | ||
vendor_contact | string · text | yes | ||
receipt_file_id | string (uuid) · uuid | yes | file | |
is_recurring | boolean · bool | |||
recurring_period | string · text | yes | ||
next_occurrence | string (date) · date | yes | ||
approval_status | string · text | pending, approved, rejected | ||
approved_by_user_id | string (uuid) · uuid | yes | app_user | |
approved_at | string (date-time) · timestamptz | yes | ||
rejection_reason | 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 | |||
recurring_until | string (date) · date | yes | ||
recurring_parent_id | string (uuid) · uuid | yes | expense |
Primary key: id
Checks:
CHECK ((amount >= 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK (((NOT is_recurring) OR (recurring_period IS NOT NULL)))CHECK (((recurring_period IS NULL) OR (recurring_period = ANY (ARRAY['weekly'::text, 'monthly'::text, 'quarterly'::text, 'yearly'::text]))))CHECK (((approval_status <> 'rejected'::text) OR (rejection_reason IS NOT NULL)))
FinanceProviderEvent
Table finance_provider_event · Resource finance_provider_events
GET /api/v1/finance_provider_eventsGET /api/v1/finance_provider_events/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
provider | string · text | stripe, iyzico, paytr | ||
external_id | string · text | |||
payment_id | string (uuid) · uuid | yes | payment | |
event_type | string · text | |||
amount | string (int64) · int8 | |||
currency | string · bpchar | max 3 | ||
psp_reference | string · text | yes | ||
payload_digest | string · text | |||
occurred_at | string (date-time) · timestamptz | |||
processed_at | string (date-time) · timestamptz |
Primary key: id
Unique: workspace_id, provider, external_id
Checks:
CHECK ((amount >= 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))
Installment
Table installment · Resource installments
GET /api/v1/installmentsGET /api/v1/installments/{id}PATCH /api/v1/installments/{id}→finance.update_installment
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
agreement_id | string (uuid) · uuid | agreement | ||
sequence | integer · int4 | |||
due_date | string (date) · date | |||
currency | string · bpchar | max 3 | ||
amount | string (int64) · int8 | |||
status | string · text | pending, paid, cancelled | ||
note | string · text | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
due_notice_at | string (date-time) · timestamptz | yes | ||
overdue_notice_at | string (date-time) · timestamptz | yes |
Primary key: id
Checks:
CHECK ((amount >= 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK ((sequence > 0))
PaymentAllocation
Table payment_allocation · Resource payment_allocations
GET /api/v1/payment_allocationsGET /api/v1/payment_allocations/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
payment_id | string (uuid) · uuid | payment | ||
target_type | string · text | installment, agreement, invoice | ||
target_id | string (uuid) · uuid | |||
currency | string · bpchar | max 3 | ||
amount | string (int64) · int8 | |||
created_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((amount <> 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))
PaymentEvent
Table payment_event · Resource payment_events
GET /api/v1/payment_eventsGET /api/v1/payment_events/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
payment_id | string (uuid) · uuid | payment | ||
type | string · text | authorization_success, authorization_failure, authorization_adjustment, authorization_request, authorization_action_required, charge_success, charge_failure, charge_back, charge_action_required, charge_request, refund_success, refund_failure, refund_reverse, refund_request, cancel_success, cancel_failure, cancel_request, info | ||
currency | string · bpchar | max 3 | ||
amount | string (int64) · int8 | |||
include_in_calculations | boolean · bool | |||
psp_reference | string · text | yes | ||
idempotency_key | string · text | yes | ||
message | string · text | yes | ||
external_url | string · text | yes | ||
app_id | string (uuid) · uuid | yes | ||
app_identifier | string · text | yes | ||
actor_user_id | string (uuid) · uuid | yes | app_user | |
occurred_at | string (date-time) · timestamptz | |||
created_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((currency ~ '^[A-Z]{3}$'::text))
PaymentProviderConfig
Table payment_provider_config · Resource payment_provider_configs
GET /api/v1/payment_provider_configsGET /api/v1/payment_provider_configs/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
provider | string · text | stripe, iyzico, paytr | ||
enabled | boolean · bool | |||
test_mode | boolean · bool | |||
position | integer · int4 | |||
display_name | string · text | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Unique: workspace_id, credential_id · workspace_id, provider
Checks:
CHECK (("position" >= 0))
Payment
Table payment · Resource payments
GET /api/v1/paymentsGET /api/v1/payments/{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 | yes | customer | |
name | string · text | yes | ||
message | string · text | yes | ||
psp_reference | string · text | yes | ||
idempotency_key | string · text | yes | ||
app_id | string (uuid) · uuid | yes | ||
app_identifier | string · text | yes | ||
external_url | string · text | yes | ||
available_actions | string[] · text[] | |||
last_refund_success | boolean · bool | |||
currency | string · bpchar | max 3 | ||
authorized_amount | string (int64) · int8 | |||
charged_amount | string (int64) · int8 | |||
refunded_amount | string (int64) · int8 | |||
canceled_amount | string (int64) · int8 | |||
authorize_pending_amount | string (int64) · int8 | |||
charge_pending_amount | string (int64) · int8 | |||
refund_pending_amount | string (int64) · int8 | |||
cancel_pending_amount | string (int64) · int8 | |||
method | string · text | card, transfer, cash, gift_card, other | ||
method_name | string · text | yes | ||
cc_first_digits | string · text | yes | ||
cc_last_digits | string · text | yes | ||
cc_brand | string · text | yes | ||
cc_exp_month | integer · int2 | yes | ||
cc_exp_year | integer · int2 | yes | ||
gift_card_last_chars | string · text | yes | ||
gift_card_brand | string · text | yes | ||
actor_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
pending_installment_id | string (uuid) · uuid | yes | installment | |
gift_card_id | string (uuid) · uuid | yes | ||
gift_card_event_id | string (uuid) · uuid | yes | ||
checkout_url | string · text | yes | ||
provider_status | string · text | yes | ||
provider_expires_at | string (date-time) · timestamptz | yes |
Primary key: id
Checks:
CHECK ((authorize_pending_amount >= 0))CHECK ((authorized_amount >= 0))CHECK ((cancel_pending_amount >= 0))CHECK ((canceled_amount >= 0))CHECK (((cc_exp_month >= 1) AND (cc_exp_month <= 12)))CHECK ((cc_exp_year >= 2000))CHECK ((charge_pending_amount >= 0))CHECK ((charged_amount >= 0))CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK (((method <> 'gift_card'::text) OR (gift_card_id IS NOT NULL)))CHECK ((refund_pending_amount >= 0))CHECK ((refunded_amount >= 0))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
finance.agreement_cancelled | An agreement was cancelled. | id, customerId |
finance.agreement_created | An agreement was opened. | id, customerId, affiliateId, currency, totalAmount, kind, sourceModule, sourceRecordId |
finance.agreement_updated | An agreement changed. | id |
finance.commission_accrued | An affiliate is owed something. | id, agreementId, affiliateId, amount, currency |
finance.commission_cancelled | A commission was cancelled. | id |
finance.commission_payout_recorded | A transfer to an affiliate paid a set of commissions. | id, affiliateId, amount, currency, count |
finance.commission_recalculated | A pending commission moved with what has been collected (K-09). | id, agreementId, from, to |
finance.commission_status_changed | A commission moved. | id, from, to |
finance.discount_applied | An agreement was discounted. | id, agreementId, amount, currency |
finance.discount_removed | A discount was taken off. | id, agreementId |
finance.expense_approved | An expense was signed off. | id |
finance.expense_category_changed | The expense vocabulary changed. | id |
finance.expense_created | An expense was recorded. | id, amount, currency |
finance.expense_paid | An expense was paid. | id |
finance.expense_rejected | An expense was turned down. | id |
finance.expense_updated | An expense was corrected. | id |
finance.expense_voided | An unpaid expense was withdrawn. | id |
finance.installment_cancelled | An instalment was taken out of the plan. | id, agreementId |
finance.installment_due | An instalment falls due within the reminder window (Finance → reminder days). Raised once per instalment. | id, agreementId, customerId, customerName, description, sequence, dueDate, amount, outstandingAmount, currency, outstandingText, daysUntilDue |
finance.installment_overdue | An instalment passed its due date with money still owed. Raised once per instalment. | id, agreementId, customerId, customerName, description, sequence, dueDate, amount, outstandingAmount, currency, outstandingText, daysOverdue |
finance.installment_updated | An instalment moved or changed. | id, agreementId |
finance.installments_scheduled | A due-date plan was written. | agreementId, count, totalAmount |
finance.payment_allocated | A collection was spread across targets. | id, count |
finance.payment_method_changed | A connected payment app was enabled, disabled, reordered or switched between test and live mode. | provider, enabled |
finance.payment_provider_event | A payment provider answered an authorize, capture or release request. | id, provider, type, amount |
finance.payment_recorded | Money arrived. | id, installmentId, agreementId, amount, currency, customerId, customerName, amountText |
finance.payment_refunded | Money went back. | id, amount, currency |
finance.refund_approved | A refund request was approved and executed. | id, paymentId, amount, currency |
finance.refund_rejected | A refund request was turned down. | id, paymentId, amount |
finance.refund_requested | A refund was asked for and waits for approval. | id, paymentId, amount, currency |