Not released yet. PeltyCMS is under development and not released yet; these docs describe the upcoming release and may change.
PeltyCMS Developer docs
User docsDevelopers
v0.1.0

Developer docs/Extension reference

Events index

All 38 core events: 34 domain events (published after the fact, observe only) and 4 pre-events (run before the action; 4 can be cancelled, 3 can be modified). On top of these, every mutating API request is a cancellable, modifiable event, see Cancel and modify. Rendered from pelty_extensions::events::REGISTRY; the test crates/pelty-api/tests/event_registry_coverage.rs fails when the source emits an event or payload key missing here. Machine-readable: events.json.

#How events work

#Categories

#All events

EventCategoryPhasesCancelModifyPayload
user.registeredUserafter, finishednonouser_id via
user.logged_inUserafter, finishednonouser_id
user.login_alertUserafter, finishednonouser_id ip user_agent
customer.closedCustomerafter, finishednonocustomer_id
order.createdOrderafter, finishednonoorder_id customer_id total currency
order.approvedOrderafter, finishednonoorder_id services auto
order.cancelledOrderafter, finishednonoorder_id fraud
order.refundedOrderafter, finishednonoorder_id credit_note_id
order.fraud_reviewOrderafter, finishednonoorder_id customer_id score
order.fraud_rejectedOrderafter, finishednonoorder_id customer_id score
invoice.createdInvoiceafter, finishednonoinvoice_id customer_id service_id renewal on_demand late_fee_for debit_for number project_id billable
invoice.paidInvoiceafter, finishednonoinvoice_id customer_id
invoice.voidedInvoiceafter, finishednonoinvoice_id
invoice.cancelledInvoiceafter, finishednonoinvoice_id reason
invoice.refundedInvoiceafter, finishednonoinvoice_id credit_note_id refund_id amount
invoice.due_soonInvoiceafter, finishednonoinvoice_id customer_id days_left
invoice.overdueInvoiceafter, finishednonoinvoice_id customer_id days_overdue
payment.succeededPaymentafter, finishednonoinvoice_id payment_id gateway provider_ref autopay
payment.failedPaymentafter, finishednonoinvoice_id payment_id gateway provider_ref autopay attempt
payment.disputedPaymentafter, finishednonogateway provider_ref payment_id event_type
payment_method.expiringPayment methodafter, finishednonopayment_method_id customer_id
service.createdServiceafter, finishednonoservice_id order_id
service.provisionedServiceafter, finishednonoservice_id action module
service.provision_failedServiceafter, finishednonoservice_id action error
service.suspendedServiceafter, finishednonoservice_id from reason
service.unsuspendedServiceafter, finishednonoservice_id from reason
service.terminatedServiceafter, finishednonoservice_id from reason provisioned
service.cancelledServiceafter, finishednonoservice_id
ticket.openedTicketafter, finishednonoticket_id number
ticket.repliedTicketafter, finishednonoticket_id by_staff
ticket.closedTicketafter, finishednonoticket_id reason
chat.startedChatafter, finishednonochat_id name email
quote.sentQuoteafter, finishednonoquote_id
affiliate.payout_requestedAffiliateafter, finishednonoaffiliate_id payout_id amount currency
login.beforeLoginbeforeyesnoemail ip user_agent
client.before_registerClientbeforeyesyesemail display_name ip
order.before_placeOrderbeforeyesyescustomer_id currency items coupon
invoice.before_createInvoicebeforeyesyescustomer_id currency finalize items

#Affiliate

#affiliate.payout_requested

An affiliate asked for a payout.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
affiliate_iduuidyesAffiliate
payout_iduuidyesPayout
amountdecimalyesAmount
currencystringyesISO 4217 code

Emitted from: crates/pelty-api/src/routes/affiliates.rs:163.

JSON
{
  "affiliate_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "amount": "12.50",
  "currency": "EUR",
  "payout_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "affiliate.payout_requested"
    ],
    "hooks": [
      {
        "event": "finished:affiliate.payout_requested",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "affiliate.payout_requested" {
        let affiliate_id = payload["affiliate_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:affiliate.payout_requested", affiliate_id);
    }
}

#Chat

#chat.started

A visitor started a live chat.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
chat_iduuidyesChat
namestringnoVisitor name
emailstringnoVisitor email

Emitted from: crates/pelty-api/src/routes/live_chat.rs:60.

JSON
{
  "chat_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "email": "[email protected]",
  "name": "…"
}
JSON
{
  "capabilities": {
    "events": [
      "chat.started"
    ],
    "hooks": [
      {
        "event": "finished:chat.started",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "chat.started" {
        let chat_id = payload["chat_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:chat.started", chat_id);
    }
}

#Client

#client.before_register

Before a sign-up. Cancel to refuse it; display_name may be rewritten. The password never leaves the host.

KindPhasesCancellableModifiableSince
pre-eventbeforeyesyes0.1.0

Payload

KeyTypeAlways presentDescription
emailstringyesSubmitted email
display_namestringyesDisplay name (modifiable)
ipstringnoClient IP

Modifiable keys (everything else in a modify is ignored)

KeyRule
display_nameString of at most 200 bytes; anything else keeps the submitted name

Emitted from: crates/pelty-api/src/routes/auth.rs:113.

JSON
{
  "display_name": "…",
  "email": "[email protected]",
  "ip": "203.0.113.7"
}
JSON
{
  "capabilities": {
    "hooks": [
      {
        "event": "client.before_register",
        "fail_open": true,
        "priority": "normal",
        "timeout_ms": 500
      }
    ]
  }
}
Rust
// op `hook`, phase `before`: return proceed, modify or cancel.
fn hook(input: &Value) -> Value {
    if input["event"] != "client.before_register" || input["cancelled"] == true {
        return reply::proceed();
    }
    let mut p = input["payload"].clone();
    // Cancel: the caller gets HTTP 409 `error.addon_hook_cancelled`.
    if p["email"].as_str().is_some_and(|v| v.ends_with(".invalid")) {
        return reply::cancel("refused by my-addon");
    }
    // Modify: only the keys listed under "Modifiable keys" are read back.
    p["display_name"] = json!(p["display_name"].as_str().unwrap_or("").trim());
    reply::modify(p)
}

#Customer

#customer.closed

A customer account was closed (GDPR erasure or admin close).

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
customer_iduuidyesCustomer

Emitted from: crates/pelty-api/src/security.rs:370.

JSON
{
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "customer.closed"
    ],
    "hooks": [
      {
        "event": "finished:customer.closed",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "customer.closed" {
        let customer_id = payload["customer_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:customer.closed", customer_id);
    }
}

#Invoice

#invoice.created

An invoice was issued (order, renewal, late fee, debit note, project or billable items).

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
invoice_iduuidyesInvoice
customer_iduuidnoCustomer
service_iduuidnoRenewed service
renewalboolnoRenewal invoice
on_demandboolnoRenewal requested early by the client
late_fee_foruuidnoOverdue invoice the fee is for
debit_foruuidnoInvoice the debit note corrects
numberstringnoInvoice number
project_iduuidnoProject
billableboolnoFrom billable items

Emitted from: crates/pelty-api/src/overdue.rs:38, crates/pelty-api/src/routes/billing.rs:214, crates/pelty-api/src/routes/billing.rs:671, crates/pelty-api/src/routes/billing.rs:700, crates/pelty-api/src/routes/billing.rs:772, crates/pelty-api/src/routes/credit.rs:147, crates/pelty-api/src/routes/projects.rs:126, crates/pelty-api/src/routes/renewals.rs:65, crates/pelty-api/src/routes/service_changes.rs:98, crates/pelty-api/src/worker.rs:40, crates/pelty-api/src/worker.rs:53.

JSON
{
  "billable": true,
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "debit_for": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "invoice_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "late_fee_for": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "number": "…",
  "on_demand": true,
  "project_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "renewal": true,
  "service_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "invoice.created"
    ],
    "hooks": [
      {
        "event": "finished:invoice.created",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "invoice.created" {
        let invoice_id = payload["invoice_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:invoice.created", invoice_id);
    }
}

#invoice.paid

An invoice became fully paid (gateway, credit, autopay or admin).

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
invoice_iduuidyesInvoice
customer_iduuidnoCustomer (only on checkout auto-paid invoices)

Emitted from: crates/pelty-api/src/autopay.rs:115, crates/pelty-api/src/routes/billing.rs:219, crates/pelty-api/src/routes/billing.rs:830, crates/pelty-api/src/routes/bulk.rs:63, crates/pelty-api/src/routes/credit.rs:53, crates/pelty-api/src/routes/credit.rs:76, crates/pelty-api/src/routes/webhooks.rs:161.

JSON
{
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "invoice_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "invoice.paid"
    ],
    "hooks": [
      {
        "event": "finished:invoice.paid",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "invoice.paid" {
        let invoice_id = payload["invoice_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:invoice.paid", invoice_id);
    }
}

#invoice.voided

An invoice was voided.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
invoice_iduuidyesInvoice

Emitted from: crates/pelty-api/src/routes/billing.rs:723, crates/pelty-api/src/routes/bulk.rs:109.

JSON
{
  "invoice_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "invoice.voided"
    ],
    "hooks": [
      {
        "event": "finished:invoice.voided",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "invoice.voided" {
        let invoice_id = payload["invoice_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:invoice.voided", invoice_id);
    }
}

#invoice.cancelled

An unpaid invoice was cancelled by automation.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
invoice_iduuidyesInvoice
reasonstringyesWhy, e.g. overdue_auto_cancel

Emitted from: crates/pelty-api/src/overdue.rs:53.

JSON
{
  "invoice_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "reason": "…"
}
JSON
{
  "capabilities": {
    "events": [
      "invoice.cancelled"
    ],
    "hooks": [
      {
        "event": "finished:invoice.cancelled",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "invoice.cancelled" {
        let invoice_id = payload["invoice_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:invoice.cancelled", invoice_id);
    }
}

#invoice.refunded

A payment on the invoice was refunded.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
invoice_iduuidyesInvoice
credit_note_iduuidnoCredit note
refund_iduuidnoRefund
amountdecimalnoRefunded amount

Emitted from: crates/pelty-api/src/routes/billing.rs:746, crates/pelty-api/src/routes/refunds.rs:164, crates/pelty-api/src/routes/store.rs:489.

JSON
{
  "amount": "12.50",
  "credit_note_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "invoice_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "refund_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "invoice.refunded"
    ],
    "hooks": [
      {
        "event": "finished:invoice.refunded",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "invoice.refunded" {
        let invoice_id = payload["invoice_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:invoice.refunded", invoice_id);
    }
}

#invoice.due_soon

Daily reminder: an invoice is due in a few days.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
invoice_iduuidyesInvoice
customer_iduuidyesCustomer
days_leftintyesDays until due

Emitted from: crates/pelty-api/src/approval.rs:77.

JSON
{
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "days_left": 3,
  "invoice_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "invoice.due_soon"
    ],
    "hooks": [
      {
        "event": "finished:invoice.due_soon",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "invoice.due_soon" {
        let invoice_id = payload["invoice_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:invoice.due_soon", invoice_id);
    }
}

#invoice.overdue

Daily reminder: an invoice is past due.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
invoice_iduuidyesInvoice
customer_iduuidyesCustomer
days_overdueintyesDays past due

Emitted from: crates/pelty-api/src/overdue.rs:35.

JSON
{
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "days_overdue": 3,
  "invoice_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "invoice.overdue"
    ],
    "hooks": [
      {
        "event": "finished:invoice.overdue",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "invoice.overdue" {
        let invoice_id = payload["invoice_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:invoice.overdue", invoice_id);
    }
}

#invoice.before_create

Before an admin creates an invoice. Line descriptions may be rewritten; amounts only with money.modify.

KindPhasesCancellableModifiableSince
pre-eventbeforeyesyes0.1.0

Payload

KeyTypeAlways presentDescription
customer_iduuidyesCustomer
currencystringyesISO 4217 code
finalizeboolyesIssue immediately
itemsarrayyes{description, quantity, unit_price} lines (decimals as strings)

Modifiable keys (everything else in a modify is ignored)

KeyRule
items[].descriptionNon-blank string of at most 500 bytes, matched by position; otherwise the original line text
items[].unit_priceNeeds capabilities.money.modify; an exact decimal string (at most 4 places), audited as addon.money_modified. Without the capability the change is reverted

Emitted from: crates/pelty-api/src/routes/billing.rs:623.

JSON
{
  "currency": "EUR",
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "finalize": true,
  "items": []
}
JSON
{
  "capabilities": {
    "hooks": [
      {
        "event": "invoice.before_create",
        "fail_open": true,
        "priority": "normal",
        "timeout_ms": 500
      }
    ]
  }
}
Rust
// op `hook`, phase `before`: return proceed, modify or cancel.
fn hook(input: &Value) -> Value {
    if input["event"] != "invoice.before_create" || input["cancelled"] == true {
        return reply::proceed();
    }
    let mut p = input["payload"].clone();
    // Cancel: the caller gets HTTP 409 `error.addon_hook_cancelled`.
    if p["customer_id"].as_str().is_some_and(|v| v.ends_with(".invalid")) {
        return reply::cancel("refused by my-addon");
    }
    // Modify: rewrite `description` on every `items` element.
    if let Some(lines) = p["items"].as_array_mut() {
        for l in lines {
            l["description"] = json!(l["description"].as_str().unwrap_or("").trim());
        }
    }
    reply::modify(p)
}

#Login

#login.before

Before a password sign-in is checked. Cancel to refuse the login (the client gets 409 error.addon_hook_cancelled). The payload is read-only: changes are ignored.

KindPhasesCancellableModifiableSince
pre-eventbeforeyesno0.1.0

Payload

KeyTypeAlways presentDescription
emailstringyesSubmitted email
ipstringnoClient IP
user_agentstringnoBrowser user agent

Modifiable keys: none. A modify reply is accepted but the host does not read anything back.

Emitted from: crates/pelty-api/src/routes/auth.rs:151.

JSON
{
  "email": "[email protected]",
  "ip": "203.0.113.7",
  "user_agent": "…"
}
JSON
{
  "capabilities": {
    "hooks": [
      {
        "event": "login.before",
        "fail_open": true,
        "priority": "normal",
        "timeout_ms": 500
      }
    ]
  }
}
Rust
// op `hook`, phase `before`: return proceed, modify or cancel.
fn hook(input: &Value) -> Value {
    if input["event"] != "login.before" || input["cancelled"] == true {
        return reply::proceed();
    }
    let mut p = input["payload"].clone();
    // Cancel: the caller gets HTTP 409 `error.addon_hook_cancelled`.
    if p["email"].as_str().is_some_and(|v| v.ends_with(".invalid")) {
        return reply::cancel("refused by my-addon");
    }
    reply::proceed() // this event is read-only: a modify is ignored
}

#Order

#order.created

A customer placed an order.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
order_iduuidyesOrder
customer_iduuidyesCustomer
totaldecimalyesOrder total
currencystringyesISO 4217 code

Emitted from: crates/pelty-api/src/routes/billing.rs:212.

JSON
{
  "currency": "EUR",
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "order_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "total": "12.50"
}
JSON
{
  "capabilities": {
    "events": [
      "order.created"
    ],
    "hooks": [
      {
        "event": "finished:order.created",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "order.created" {
        let order_id = payload["order_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:order.created", order_id);
    }
}

#order.approved

An order was accepted and its services created.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
order_iduuidyesOrder
servicesarrayyesCreated service ids
autoboolnoApproved automatically on payment

Emitted from: crates/pelty-api/src/approval.rs:17, crates/pelty-api/src/routes/billing.rs:498, crates/pelty-api/src/routes/store.rs:441.

JSON
{
  "auto": true,
  "order_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "services": []
}
JSON
{
  "capabilities": {
    "events": [
      "order.approved"
    ],
    "hooks": [
      {
        "event": "finished:order.approved",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "order.approved" {
        let order_id = payload["order_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:order.approved", order_id);
    }
}

#order.cancelled

An order was cancelled.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
order_iduuidyesOrder
fraudboolnoCancelled by the fraud check

Emitted from: crates/pelty-api/src/routes/billing.rs:535, crates/pelty-api/src/routes/bulk.rs:216.

JSON
{
  "fraud": true,
  "order_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "order.cancelled"
    ],
    "hooks": [
      {
        "event": "finished:order.cancelled",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "order.cancelled" {
        let order_id = payload["order_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:order.cancelled", order_id);
    }
}

#order.refunded

A paid order was refunded with a credit note.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
order_iduuidyesOrder
credit_note_iduuidyesCredit note

Emitted from: crates/pelty-api/src/routes/store.rs:490.

JSON
{
  "credit_note_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "order_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "order.refunded"
    ],
    "hooks": [
      {
        "event": "finished:order.refunded",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "order.refunded" {
        let order_id = payload["order_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:order.refunded", order_id);
    }
}

#order.fraud_review

The fraud check held an order for review.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
order_iduuidyesOrder
customer_iduuidyesCustomer
scoredecimalyesFraud score

Emitted from: crates/pelty-api/src/routes/billing.rs:207.

JSON
{
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "order_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "score": "12.50"
}
JSON
{
  "capabilities": {
    "events": [
      "order.fraud_review"
    ],
    "hooks": [
      {
        "event": "finished:order.fraud_review",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "order.fraud_review" {
        let order_id = payload["order_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:order.fraud_review", order_id);
    }
}

#order.fraud_rejected

The fraud check rejected an order.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
order_iduuidyesOrder
customer_iduuidyesCustomer
scoredecimalyesFraud score

Emitted from: crates/pelty-api/src/routes/billing.rs:202.

JSON
{
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "order_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "score": "12.50"
}
JSON
{
  "capabilities": {
    "events": [
      "order.fraud_rejected"
    ],
    "hooks": [
      {
        "event": "finished:order.fraud_rejected",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "order.fraud_rejected" {
        let order_id = payload["order_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:order.fraud_rejected", order_id);
    }
}

#order.before_place

Before an order is placed. Cancel to refuse; coupon may be set or cleared.

KindPhasesCancellableModifiableSince
pre-eventbeforeyesyes0.1.0

Payload

KeyTypeAlways presentDescription
customer_iduuidyesCustomer
currencystringyesISO 4217 code
itemsarrayyesCart lines
couponstringnoCoupon code (modifiable)

Modifiable keys (everything else in a modify is ignored)

KeyRule
couponNon-empty string of at most 64 bytes sets the code, null removes it, any other value keeps the submitted code. The code is then validated like a typed one

Emitted from: crates/pelty-api/src/routes/billing.rs:171.

JSON
{
  "coupon": "…",
  "currency": "EUR",
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "items": []
}
JSON
{
  "capabilities": {
    "hooks": [
      {
        "event": "order.before_place",
        "fail_open": true,
        "priority": "normal",
        "timeout_ms": 500
      }
    ]
  }
}
Rust
// op `hook`, phase `before`: return proceed, modify or cancel.
fn hook(input: &Value) -> Value {
    if input["event"] != "order.before_place" || input["cancelled"] == true {
        return reply::proceed();
    }
    let mut p = input["payload"].clone();
    // Cancel: the caller gets HTTP 409 `error.addon_hook_cancelled`.
    if p["customer_id"].as_str().is_some_and(|v| v.ends_with(".invalid")) {
        return reply::cancel("refused by my-addon");
    }
    // Modify: only the keys listed under "Modifiable keys" are read back.
    p["coupon"] = json!(p["coupon"].as_str().unwrap_or("").trim());
    reply::modify(p)
}

#Payment

#payment.succeeded

A payment was captured.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
invoice_iduuidnoInvoice
payment_iduuidnoPayment
gatewaystringnoGateway id
provider_refstringnoGateway reference
autopayboolnoCharged by autopay

Emitted from: crates/pelty-api/src/autopay.rs:113, crates/pelty-api/src/routes/billing.rs:825, crates/pelty-api/src/routes/bulk.rs:61, crates/pelty-api/src/routes/credit.rs:51, crates/pelty-api/src/routes/credit.rs:74, crates/pelty-api/src/routes/webhooks.rs:156, crates/pelty-api/src/routes/webhooks.rs:168.

JSON
{
  "autopay": true,
  "gateway": "stripe",
  "invoice_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "payment_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "provider_ref": "…"
}
JSON
{
  "capabilities": {
    "events": [
      "payment.succeeded"
    ],
    "hooks": [
      {
        "event": "finished:payment.succeeded",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "payment.succeeded" {
        let invoice_id = payload["invoice_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:payment.succeeded", invoice_id);
    }
}

#payment.failed

A payment attempt failed.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
invoice_iduuidnoInvoice
payment_iduuidnoPayment
gatewaystringnoGateway id
provider_refstringnoGateway reference
autopayboolnoAutopay attempt
attemptintnoAutopay attempt number

Emitted from: crates/pelty-api/src/autopay.rs:128, crates/pelty-api/src/routes/webhooks.rs:175.

JSON
{
  "attempt": 3,
  "autopay": true,
  "gateway": "stripe",
  "invoice_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "payment_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "provider_ref": "…"
}
JSON
{
  "capabilities": {
    "events": [
      "payment.failed"
    ],
    "hooks": [
      {
        "event": "finished:payment.failed",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "payment.failed" {
        let invoice_id = payload["invoice_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:payment.failed", invoice_id);
    }
}

#payment.disputed

A gateway reported a chargeback or dispute.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
gatewaystringyesGateway id
provider_refstringyesGateway reference
payment_iduuidnoPayment
event_typestringnoGateway event type

Emitted from: crates/pelty-api/src/routes/webhooks.rs:124.

JSON
{
  "event_type": "…",
  "gateway": "stripe",
  "payment_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "provider_ref": "…"
}
JSON
{
  "capabilities": {
    "events": [
      "payment.disputed"
    ],
    "hooks": [
      {
        "event": "finished:payment.disputed",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "payment.disputed" {
        let gateway = payload["gateway"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:payment.disputed", gateway);
    }
}

#Payment method

#payment_method.expiring

A saved card expires soon.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
payment_method_iduuidyesPayment method
customer_iduuidyesCustomer

Emitted from: crates/pelty-api/src/autopay.rs:64.

JSON
{
  "customer_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "payment_method_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "payment_method.expiring"
    ],
    "hooks": [
      {
        "event": "finished:payment_method.expiring",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "payment_method.expiring" {
        let payment_method_id = payload["payment_method_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:payment_method.expiring", payment_method_id);
    }
}

#Quote

#quote.sent

A quote was sent to a customer.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
quote_iduuidyesQuote

Emitted from: crates/pelty-api/src/routes/quotes.rs:158.

JSON
{
  "quote_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "quote.sent"
    ],
    "hooks": [
      {
        "event": "finished:quote.sent",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "quote.sent" {
        let quote_id = payload["quote_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:quote.sent", quote_id);
    }
}

#Service

#service.created

A service was created from an order.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
service_iduuidyesService
order_iduuidyesOrder

Emitted from: crates/pelty-api/src/approval.rs:19, crates/pelty-api/src/routes/billing.rs:503, crates/pelty-api/src/routes/store.rs:443.

JSON
{
  "order_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "service_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "service.created"
    ],
    "hooks": [
      {
        "event": "finished:service.created",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "service.created" {
        let service_id = payload["service_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:service.created", service_id);
    }
}

#service.provisioned

A provisioning module action succeeded.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
service_iduuidyesService
actionstringyescreate, suspend, …
modulestringyesModule id

Emitted from: crates/pelty-api/src/provision.rs:120.

JSON
{
  "action": "…",
  "module": "…",
  "service_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "service.provisioned"
    ],
    "hooks": [
      {
        "event": "finished:service.provisioned",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "service.provisioned" {
        let service_id = payload["service_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:service.provisioned", service_id);
    }
}

#service.provision_failed

A provisioning module action failed.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
service_iduuidyesService
actionstringyesModule action
errorstringyesError message

Emitted from: crates/pelty-api/src/provision.rs:142.

JSON
{
  "action": "…",
  "error": "…",
  "service_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "service.provision_failed"
    ],
    "hooks": [
      {
        "event": "finished:service.provision_failed",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "service.provision_failed" {
        let service_id = payload["service_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:service.provision_failed", service_id);
    }
}

#service.suspended

A service was suspended.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
service_iduuidyesService
fromstringnoPrevious status
reasonstringnoWhy

Emitted from: crates/pelty-api/src/overdue.rs:42, crates/pelty-api/src/routes/billing.rs:899, crates/pelty-api/src/routes/refunds.rs:151.

JSON
{
  "from": "…",
  "reason": "…",
  "service_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "service.suspended"
    ],
    "hooks": [
      {
        "event": "finished:service.suspended",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "service.suspended" {
        let service_id = payload["service_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:service.suspended", service_id);
    }
}

#service.unsuspended

A service was reactivated.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
service_iduuidyesService
fromstringnoPrevious status
reasonstringnoWhy

Emitted from: crates/pelty-api/src/overdue.rs:65, crates/pelty-api/src/routes/billing.rs:900.

JSON
{
  "from": "…",
  "reason": "…",
  "service_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "service.unsuspended"
    ],
    "hooks": [
      {
        "event": "finished:service.unsuspended",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "service.unsuspended" {
        let service_id = payload["service_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:service.unsuspended", service_id);
    }
}

#service.terminated

A service was terminated.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
service_iduuidyesService
fromstringnoPrevious status
reasonstringnoWhy
provisionedboolnoThe module terminated it remotely

Emitted from: crates/pelty-api/src/overdue.rs:50, crates/pelty-api/src/routes/billable.rs:131, crates/pelty-api/src/routes/billing.rs:901.

JSON
{
  "from": "…",
  "provisioned": true,
  "reason": "…",
  "service_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "service.terminated"
    ],
    "hooks": [
      {
        "event": "finished:service.terminated",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "service.terminated" {
        let service_id = payload["service_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:service.terminated", service_id);
    }
}

#service.cancelled

A cancellation request was applied.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
service_iduuidyesService

Emitted from: crates/pelty-api/src/routes/billing.rs:902, crates/pelty-api/src/routes/service_changes.rs:63.

JSON
{
  "service_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "service.cancelled"
    ],
    "hooks": [
      {
        "event": "finished:service.cancelled",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "service.cancelled" {
        let service_id = payload["service_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:service.cancelled", service_id);
    }
}

#Ticket

#ticket.opened

A support ticket was opened.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
ticket_iduuidyesTicket
numberstringnoTicket number

Emitted from: crates/pelty-api/src/routes/tickets.rs:308.

JSON
{
  "number": "…",
  "ticket_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "ticket.opened"
    ],
    "hooks": [
      {
        "event": "finished:ticket.opened",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "ticket.opened" {
        let ticket_id = payload["ticket_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:ticket.opened", ticket_id);
    }
}

#ticket.replied

A reply was posted on a ticket.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
ticket_iduuidyesTicket
by_staffboolyesReply came from staff

Emitted from: crates/pelty-api/src/routes/tickets.rs:352, crates/pelty-api/src/routes/tickets.rs:588.

JSON
{
  "by_staff": true,
  "ticket_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "ticket.replied"
    ],
    "hooks": [
      {
        "event": "finished:ticket.replied",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "ticket.replied" {
        let ticket_id = payload["ticket_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:ticket.replied", ticket_id);
    }
}

#ticket.closed

A ticket was closed.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
ticket_iduuidyesTicket
reasonstringnoWhy, e.g. inactive

Emitted from: crates/pelty-api/src/routes/support.rs:418.

JSON
{
  "reason": "…",
  "ticket_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "ticket.closed"
    ],
    "hooks": [
      {
        "event": "finished:ticket.closed",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "ticket.closed" {
        let ticket_id = payload["ticket_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:ticket.closed", ticket_id);
    }
}

#User

#user.registered

A new account was created (password sign-up, SSO or OAuth).

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
user_iduuidyesNew user
viastringnoSSO/OAuth provider id when not a password sign-up

Emitted from: crates/pelty-api/src/oauth.rs:285, crates/pelty-api/src/routes/auth.rs:132, crates/pelty-api/src/saml.rs:354.

JSON
{
  "user_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e",
  "via": "…"
}
JSON
{
  "capabilities": {
    "events": [
      "user.registered"
    ],
    "hooks": [
      {
        "event": "finished:user.registered",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "user.registered" {
        let user_id = payload["user_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:user.registered", user_id);
    }
}

#user.logged_in

A user signed in successfully.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
user_iduuidyesUser

Emitted from: crates/pelty-api/src/routes/auth.rs:203.

JSON
{
  "user_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "user.logged_in"
    ],
    "hooks": [
      {
        "event": "finished:user.logged_in",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "user.logged_in" {
        let user_id = payload["user_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:user.logged_in", user_id);
    }
}

#user.login_alert

Sign-in from a new IP for an account with login alerts on.

KindPhasesCancellableModifiableSince
domain eventafter, finishednono0.1.0

Payload

KeyTypeAlways presentDescription
user_iduuidyesUser
ipstringyesClient IP
user_agentstringyesBrowser user agent

Emitted from: crates/pelty-api/src/routes/auth.rs:340.

JSON
{
  "ip": "203.0.113.7",
  "user_agent": "…",
  "user_id": "0190f7a2-6c1e-7b3a-9d2e-4f5a6b7c8d9e"
}
JSON
{
  "capabilities": {
    "events": [
      "user.login_alert"
    ],
    "hooks": [
      {
        "event": "finished:user.login_alert",
        "priority": "monitor"
      }
    ]
  }
}
Rust
// on-event: delivered after the action was committed (cannot be cancelled).
fn on_event(name: &str, payload: &Value) {
    if name == "user.login_alert" {
        let user_id = payload["user_id"].as_str().unwrap_or_default();
        let _ = host::kv_set("last:user.login_alert", user_id);
    }
}

#Custom addon events

Named <addon-id>:<event>, declared in capabilities.emits and emitted with pelty_addon_sdk::emit. They are not in the registry because addons define them. Other addons listen with before:<id>:<event> (cancel or modify), after: or finished: in hooks. An emit inside a listener nests; the depth is limited to 4. The emitter gets {cancelled, reason, payload} back. See Events and hooks for a full example.

#Lifecycle phases

Phases in order: before, during, after, finished. Source: crates/pelty-api/src/addon_events.rs.

#Webhook plugins

A webhook plugin lists domain events in its manifest hooks. Each one is POSTed to webhook_url, signed, with the payload above. Delivery attempts are listed under Admin, Plugins, Deliveries.

FieldTypeRequiredDescription
idStringyes
nameStringyes
versionStringyes
api_versionu32yes
descriptionStringno
capabilitiesVec<String>no
hooksVec<String>no
webhook_urlOption<String>noExternal plugins only: HTTPS endpoint receiving signed events.
settings_schemaOption<serde_json::Value>noOptional JSON-schema-like description of admin settings.
uiPluginUinoUI contributions rendered by the frontend in named slots.

Source: crates/pelty-extensions/src/plugins.rs (PluginManifest).

JSON
{
  "api_version": 1,
  "hooks": [
    "invoice.paid",
    "order.created"
  ],
  "id": "my-plugin",
  "name": "My plugin",
  "version": "1.0.0",
  "webhook_url": "https://example.com/pelty"
}