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

Cancel and modify

Everything an addon can stop or change before it happens, and exactly what it may change. There are three kinds: typed pre-events, request events on every mutating API route, and mixins.

#Rules

#Reply format

JSON
{"action":"continue"}
{"action":"modify","payload":{ "coupon": "SUMMER" }}
{"action":"cancel","reason":"not allowed"}
{"action":"uncancel"}

The Rust SDK builds these with reply::proceed(), reply::modify(p), reply::cancel(reason) and reply::uncancel().

#Typed pre-events

EventCancelModifiable keys and rulesPayload
login.beforeyesnone (read-only)email, ip, user_agent
client.before_registeryesdisplay_name: String of at most 200 bytes; anything else keeps the submitted nameemail, display_name, ip
order.before_placeyescoupon: Non-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 onecustomer_id, currency, items, coupon
invoice.before_createyesitems[].description: Non-blank string of at most 500 bytes, matched by position; otherwise the original line text; items[].unit_price: Needs capabilities.money.modify; an exact decimal string (at most 4 places), audited as addon.money_modified. Without the capability the change is revertedcustomer_id, currency, finalize, items

#Example: cancel, modify and uncancel

extensions/examples/addon-guide/src/lib.rs · snippet hooksRust
/// Pre-events and lifecycle phases listed in `capabilities.hooks` call op
/// `hook`. Return `proceed`, `modify(payload)` or `cancel(reason)`.
fn hook(input: &Value) -> Value {
    let event = input["event"].as_str().unwrap_or("");
    if input["phase"].as_str().unwrap_or("before") != "before" {
        return reply::proceed(); // during/after/finished only observe
    }
    let mut p = input["payload"].clone();
    bump(&format!("hook:{event}"));
    match event {
        // Cancel: refuse sign-in for one domain.
        "login.before" if p["email"].as_str().is_some_and(|e| e.ends_with("@denied.test")) => reply::cancel("domain not allowed"),
        // Modify: tidy up the display name before the account is created.
        "client.before_register" => {
            p["display_name"] = json!(p["display_name"].as_str().unwrap_or("").trim().to_string());
            reply::modify(p)
        }
        // Own custom event (declared as `before:integration-guide:greeted`,
        // delivered with `event` = the bare name and `phase` = "before").
        "integration-guide:greeted" if p["name"] == "nobody" => reply::cancel("no greeting for nobody"),
        "order.before_place" => order_hook(input, p),
        _ => reply::proceed(),
    }
}
extensions/examples/addon-guide/src/lib.rs · snippet uncancelRust
/// `order.before_place` with `ignore_cancelled: true` and priority `highest`:
/// this runs even after an earlier addon cancelled the order. `uncancel` is
/// only honoured for such hooks. Here: always allow orders using the
/// `GUIDE-VIP` coupon, and normalise coupon codes to upper case (`coupon` is
/// the only key the host reads back from this event).
fn order_hook(input: &Value, mut p: Value) -> Value {
    let coupon = p["coupon"].as_str().unwrap_or("").trim().to_uppercase();
    if input["cancelled"] == true {
        return if coupon == "GUIDE-VIP" { reply::uncancel() } else { reply::proceed() };
    }
    if coupon.is_empty() {
        return reply::proceed();
    }
    p["coupon"] = json!(coupon);
    reply::modify(p)
}

#Request events (every mutating route)

Every POST, PUT, PATCH and DELETE under /api/v1 (except addon routes) is an event named <METHOD> <route template>. A hook before:<METHOD> <route> gets {method, path, route, query, body} and may cancel the request or rewrite body (the JSON request body, at most 1 MiB, money and secret rules apply). Patterns: before:* (every request), before:POST /api/v1/admin/* (prefix), or an exact template. Because they see every request, these hooks are a dangerous capability (hooks.any) the admin must approve at install. during, after (with the JSON response) and finished (with status and http_status) observe.

JSON
{
  "capabilities": {
    "hooks": [
      {
        "event": "before:POST /api/v1/client/tickets",
        "priority": "high"
      },
      {
        "event": "after:PUT /api/v1/client/profile",
        "priority": "monitor"
      }
    ]
  }
}

459 cancellable request events:

Hook eventAction
before:DELETE /api/v1/account/identities/{id}unlink
before:DELETE /api/v1/account/oauth-apps/{id}my revoke
before:DELETE /api/v1/account/privacy/erasure
before:POST /api/v1/account/privacy/erasure
before:DELETE /api/v1/account/security-question
before:PUT /api/v1/account/security-question
before:POST /api/v1/account/tos/acceptaccept tos
before:PUT /api/v1/admin/addons/registry/settingsput registry settings
before:POST /api/v1/admin/addons/registry/{id}/installregistry install
before:POST /api/v1/admin/addons/uploadupload
before:DELETE /api/v1/admin/addons/{id}uninstall
before:POST /api/v1/admin/addons/{id}/disabledisable
before:POST /api/v1/admin/addons/{id}/enableenable
before:POST /api/v1/admin/addons/{id}/installinstall
before:POST /api/v1/admin/addons/{id}/mixinsmixins toggle
before:PUT /api/v1/admin/addons/{id}/settingsput settings
before:POST /api/v1/admin/affiliates/materials
before:DELETE /api/v1/admin/affiliates/materials/{id}
before:PUT /api/v1/admin/affiliates/materials/{id}
before:POST /api/v1/admin/affiliates/payouts/{id}
before:PUT /api/v1/admin/affiliates/product-rates
before:DELETE /api/v1/admin/affiliates/product-rates/{product_id}
before:POST /api/v1/admin/affiliates/tiers
before:DELETE /api/v1/admin/affiliates/tiers/{id}
before:PUT /api/v1/admin/affiliates/{id}
before:POST /api/v1/admin/announcements
before:DELETE /api/v1/admin/announcements/{id}
before:PUT /api/v1/admin/announcements/{id}
before:DELETE /api/v1/admin/api-keys/{id}
before:POST /api/v1/admin/automation/{kind}/run
before:PUT /api/v1/admin/automation/{kind}/settingsput setting
before:POST /api/v1/admin/autopay/runRun auto-pay now (idempotent with the daily job).
before:POST /api/v1/admin/backups
before:DELETE /api/v1/admin/backups/{id}
before:POST /api/v1/admin/banned-emailsadd
before:DELETE /api/v1/admin/banned-emails/{domain}remove
before:POST /api/v1/admin/billable-items
before:DELETE /api/v1/admin/billable-items/{id}
before:POST /api/v1/admin/bundles
before:DELETE /api/v1/admin/bundles/{id}
before:PUT /api/v1/admin/bundles/{id}
before:POST /api/v1/admin/campaigns
before:POST /api/v1/admin/campaigns/preview
before:DELETE /api/v1/admin/campaigns/{id}
before:PUT /api/v1/admin/campaigns/{id}
before:POST /api/v1/admin/campaigns/{id}/cancel
before:DELETE /api/v1/admin/campaigns/{id}/schedule
before:POST /api/v1/admin/campaigns/{id}/scheduleHold a draft until send_at; the worker snapshots recipients then.
before:POST /api/v1/admin/campaigns/{id}/send
before:POST /api/v1/admin/cancellations/{id}/decide
before:POST /api/v1/admin/catalog/addons
before:PUT /api/v1/admin/catalog/addons/{id}
before:POST /api/v1/admin/catalog/groups
before:POST /api/v1/admin/catalog/products
before:PATCH /api/v1/admin/catalog/products/{id}
before:PUT /api/v1/admin/catalog/products/{id}/billing
before:POST /api/v1/admin/catalog/products/{id}/fields
before:DELETE /api/v1/admin/catalog/products/{id}/fields/{field}
before:PUT /api/v1/admin/catalog/products/{id}/fields/{field}
before:PUT /api/v1/admin/catalog/products/{id}/fixed-term
before:POST /api/v1/admin/catalog/products/{id}/options
before:DELETE /api/v1/admin/catalog/products/{id}/options/{option}
before:PUT /api/v1/admin/catalog/products/{id}/prices
before:PUT /api/v1/admin/catalog/products/{id}/recommendations
before:PUT /api/v1/admin/catalog/products/{id}/stock
before:POST /api/v1/admin/chats/{id}/close
before:POST /api/v1/admin/chats/{id}/messages
before:POST /api/v1/admin/client-fields
before:DELETE /api/v1/admin/client-fields/{id}
before:PUT /api/v1/admin/client-fields/{id}
before:POST /api/v1/admin/client-group-rulescreate
before:POST /api/v1/admin/client-group-rules/applyapply
before:DELETE /api/v1/admin/client-group-rules/{id}remove
before:PUT /api/v1/admin/client-group-rules/{id}update
before:POST /api/v1/admin/client-groups
before:DELETE /api/v1/admin/client-groups/{id}
before:PUT /api/v1/admin/client-groups/{id}
before:POST /api/v1/admin/client-groups/{id}/members
before:DELETE /api/v1/admin/client-groups/{id}/members/{customer_id}
before:DELETE /api/v1/admin/contact-messages/{id}contact del
before:PUT /api/v1/admin/contact-messages/{id}contact status
before:POST /api/v1/admin/contact-messages/{id}/ticketcontact to ticket
before:POST /api/v1/admin/coupons
before:DELETE /api/v1/admin/coupons/{id}
before:PUT /api/v1/admin/coupons/{id}
before:POST /api/v1/admin/currencies
before:PUT /api/v1/admin/currencies/rates/settings
before:POST /api/v1/admin/currencies/rates/update
before:POST /api/v1/admin/currencies/sync-prices
before:PUT /api/v1/admin/currencies/{code}
before:POST /api/v1/admin/currencies/{code}/default
before:PUT /api/v1/admin/custom-code
before:POST /api/v1/admin/customers
before:PATCH /api/v1/admin/customers/{id}
before:PUT /api/v1/admin/customers/{id}/auto-pay
before:POST /api/v1/admin/customers/{id}/billable-items/invoice
before:POST /api/v1/admin/customers/{id}/closeclose customer
before:POST /api/v1/admin/customers/{id}/credit
before:PUT /api/v1/admin/customers/{id}/fields
before:POST /api/v1/admin/customers/{id}/group
before:PUT /api/v1/admin/customers/{id}/kyc
before:PUT /api/v1/admin/customers/{id}/legal-entity
before:POST /api/v1/admin/customers/{id}/login-aslogin as customer
before:POST /api/v1/admin/customers/{id}/mergemerge customer
before:POST /api/v1/admin/customers/{id}/notes
before:DELETE /api/v1/admin/customers/{id}/notes/{note}
before:PUT /api/v1/admin/customers/{id}/notes/{note}
before:POST /api/v1/admin/customers/{id}/security-question/verifyCheck a caller's answer. Rate limited per customer and audited.
before:POST /api/v1/admin/customers/{id}/vat/verify
before:POST /api/v1/admin/domains/bulkadmin bulk
before:PUT /api/v1/admin/domains/margins
before:PUT /api/v1/admin/domains/premium-settings
before:POST /api/v1/admin/domains/run
before:POST /api/v1/admin/domains/sync-prices
before:PUT /api/v1/admin/domains/tld-fields/{tld}
before:PATCH /api/v1/admin/domains/{id}
before:PUT /api/v1/admin/domains/{id}/dnsadmin dns put
before:POST /api/v1/admin/domains/{id}/registrant-verification
before:POST /api/v1/admin/domains/{id}/ssl-check
before:POST /api/v1/admin/domains/{id}/sync
before:POST /api/v1/admin/downloads
before:DELETE /api/v1/admin/downloads/{id}
before:POST /api/v1/admin/email-log/{id}/resendadmin resend
before:PUT /api/v1/admin/email-templates/{name}/enabledSwitch a template on or off; disabled templates are never queued.
before:DELETE /api/v1/admin/email-templates/{name}/{locale}
before:PUT /api/v1/admin/email-templates/{name}/{locale}
before:POST /api/v1/admin/email-templates/{name}/{locale}/preview
before:POST /api/v1/admin/email-templates/{name}/{locale}/testQueue the saved template, with sample data, to the signed-in admin.
before:POST /api/v1/admin/emails/{job}/resend
before:POST /api/v1/admin/gateways/custom
before:DELETE /api/v1/admin/gateways/{id}
before:PUT /api/v1/admin/gateways/{id}
before:PUT /api/v1/admin/gateways/{id}/rules
before:POST /api/v1/admin/gateways/{id}/test-webhookVerify + parse a pasted webhook without applying it (logged as a test).
before:POST /api/v1/admin/gdpr/requests/{id}/approve
before:POST /api/v1/admin/gdpr/requests/{id}/reject
before:PUT /api/v1/admin/general-settings
before:POST /api/v1/admin/gift-cards
before:POST /api/v1/admin/gift-cards/{id}/disable
before:POST /api/v1/admin/import/csv/analyzeHeaders, a 5-row preview, the target fields and a suggested mapping.
before:POST /api/v1/admin/import/csv/runValidate every row; unless dry_run, import them all-or-nothing.
before:POST /api/v1/admin/inactive-clients/runRun the erasure now with the saved settings (no-op when disabled).
before:POST /api/v1/admin/invoices
before:POST /api/v1/admin/invoices/bulkApply one action to many invoices; returns a per-invoice result.
before:POST /api/v1/admin/invoices/merge
before:POST /api/v1/admin/invoices/{id}/apply-credit
before:POST /api/v1/admin/invoices/{id}/credit-note
before:POST /api/v1/admin/invoices/{id}/debit-noteIssue a debit note (extra charge) against an unpaid or paid invoice.
before:POST /api/v1/admin/invoices/{id}/duplicate
before:POST /api/v1/admin/invoices/{id}/finalize
before:POST /api/v1/admin/invoices/{id}/items
before:PUT /api/v1/admin/invoices/{id}/notes
before:POST /api/v1/admin/invoices/{id}/paymentsRecord a manual payment (bank transfer…). Idempotent.
before:POST /api/v1/admin/invoices/{id}/split
before:POST /api/v1/admin/invoices/{id}/void
before:POST /api/v1/admin/jobs/recurringEnqueue a recurring-billing run now (deduplicated per minute).
before:POST /api/v1/admin/jobs/{id}/retry
before:POST /api/v1/admin/kb/articles
before:DELETE /api/v1/admin/kb/articles/{id}
before:PUT /api/v1/admin/kb/articles/{id}
before:PUT /api/v1/admin/kb/articles/{id}/tagsput tags
before:POST /api/v1/admin/kb/categories
before:DELETE /api/v1/admin/kb/categories/{id}
before:PUT /api/v1/admin/kb/categories/{id}
before:POST /api/v1/admin/kyc/documents/{id}/review
before:PUT /api/v1/admin/kyc/settings
before:POST /api/v1/admin/legal-entities
before:DELETE /api/v1/admin/legal-entities/default
before:DELETE /api/v1/admin/legal-entities/{id}
before:PUT /api/v1/admin/legal-entities/{id}
before:POST /api/v1/admin/legal-entities/{id}/default
before:POST /api/v1/admin/mass/customers
before:POST /api/v1/admin/mass/domains
before:POST /api/v1/admin/mass/services
before:POST /api/v1/admin/mass/tickets
before:POST /api/v1/admin/module-queue/{id}/retry
before:PUT /api/v1/admin/my-notes
before:POST /api/v1/admin/network-issues
before:DELETE /api/v1/admin/network-issues/{id}
before:PUT /api/v1/admin/network-issues/{id}
before:POST /api/v1/admin/network-issues/{id}/updates
before:POST /api/v1/admin/noticeboard
before:DELETE /api/v1/admin/noticeboard/{id}Authors delete their own notices; staff with settings access can delete any.
before:POST /api/v1/admin/notification-rules
before:POST /api/v1/admin/notification-rules/previewPayload that would be POSTed for an event (no network call).
before:DELETE /api/v1/admin/notification-rules/{id}
before:PUT /api/v1/admin/notification-rules/{id}
before:DELETE /api/v1/admin/notification-rules/{id}/secretStop signing a webhook.
before:POST /api/v1/admin/notification-rules/{id}/secretRotate the HMAC signing secret of a generic webhook; returned once.
before:POST /api/v1/admin/notification-rules/{id}/testQueue a test delivery through the real channel.
before:POST /api/v1/admin/oauth-clientsadmin create
before:DELETE /api/v1/admin/oauth-clients/{id}admin delete
before:PATCH /api/v1/admin/oauth-clients/{id}admin update
before:POST /api/v1/admin/oauth-clients/{id}/secretadmin rotate
before:POST /api/v1/admin/order-formsadmin create
before:DELETE /api/v1/admin/order-forms/{id}admin delete
before:PUT /api/v1/admin/order-forms/{id}admin update
before:POST /api/v1/admin/ordersManual order for a customer, priced from the catalog like a client order.
before:POST /api/v1/admin/orders/bulk
before:DELETE /api/v1/admin/orders/{id}
before:POST /api/v1/admin/orders/{id}/approve
before:POST /api/v1/admin/orders/{id}/cancel
before:POST /api/v1/admin/orders/{id}/notes
before:POST /api/v1/admin/orders/{id}/pending
before:POST /api/v1/admin/orders/{id}/refundRefund an order: credit the paid invoice in full (credit note) and close
before:POST /api/v1/admin/overdue/runRun today's overdue automation now (idempotent with the daily job).
before:POST /api/v1/admin/payments/{id}/refunds
before:PUT /api/v1/admin/plugins/{id}
before:POST /api/v1/admin/plugins/{id}/deliveries/{did}/redeliver
before:PUT /api/v1/admin/plugins/{id}/settings
before:PUT /api/v1/admin/plugins/{id}/webhook
before:POST /api/v1/admin/plugins/{id}/webhook/secretGenerate a new signing secret and return it once.
before:PUT /api/v1/admin/prefs
before:PUT /api/v1/admin/product-groups/{id}/order-formadmin set group
before:PUT /api/v1/admin/products/{id}/free-domainadmin free put
before:PUT /api/v1/admin/products/{id}/module
before:PUT /api/v1/admin/products/{id}/server-groupproduct group set
before:POST /api/v1/admin/projects
before:DELETE /api/v1/admin/projects/{id}
before:PUT /api/v1/admin/projects/{id}
before:POST /api/v1/admin/projects/{id}/invoice-timeBill all unbilled billable time on one invoice.
before:DELETE /api/v1/admin/projects/{id}/links
before:POST /api/v1/admin/projects/{id}/links
before:POST /api/v1/admin/projects/{id}/tasks
before:DELETE /api/v1/admin/projects/{id}/tasks/{task}
before:PUT /api/v1/admin/projects/{id}/tasks/{task}
before:POST /api/v1/admin/projects/{id}/time
before:DELETE /api/v1/admin/projects/{id}/time/{entry}
before:POST /api/v1/admin/quotes
before:DELETE /api/v1/admin/quotes/{id}
before:PUT /api/v1/admin/quotes/{id}
before:POST /api/v1/admin/quotes/{id}/send
before:POST /api/v1/admin/roles
before:DELETE /api/v1/admin/roles/{id}
before:PUT /api/v1/admin/roles/{id}
before:POST /api/v1/admin/saml/idpsadmin create
before:DELETE /api/v1/admin/saml/idps/{id}admin delete
before:PUT /api/v1/admin/saml/idps/{id}admin update
before:POST /api/v1/admin/saml/idps/{id}/rotate-keyadmin rotate key
before:POST /api/v1/admin/saml/idps/{id}/validateadmin validate
before:POST /api/v1/admin/saml/metadataadmin parse metadata
before:POST /api/v1/admin/security-questions
before:DELETE /api/v1/admin/security-questions/{id}
before:PUT /api/v1/admin/security/captchaput policy
before:PUT /api/v1/admin/security/duo
before:POST /api/v1/admin/security/duo/test
before:POST /api/v1/admin/security/ip-rulesadd rule
before:DELETE /api/v1/admin/security/ip-rules/{id}delete rule
before:PUT /api/v1/admin/security/policyput policy
before:PUT /api/v1/admin/security/staff-2faTurning the policy on requires the actor to have 2FA (no self lock-out).
before:POST /api/v1/admin/security/tospublish tos
before:PUT /api/v1/admin/security/trusted-proxies
before:POST /api/v1/admin/server-groupsgroups create
before:DELETE /api/v1/admin/server-groups/{id}groups delete
before:PUT /api/v1/admin/server-groups/{id}groups update
before:PUT /api/v1/admin/server-groups/{id}/serversgroups set members
before:POST /api/v1/admin/servers
before:POST /api/v1/admin/servers/health/check
before:DELETE /api/v1/admin/servers/{id}
before:PUT /api/v1/admin/servers/{id}
before:POST /api/v1/admin/servers/{id}/importCreate services for panel accounts (no provisioning call; status ok).
before:POST /api/v1/admin/servers/{id}/testCheck the stored credentials against the panel. Returns {ok, error?}
before:POST /api/v1/admin/servers/{id}/vendor-catalog/syncPull the vendor catalog through the server's module.
before:POST /api/v1/admin/services/{id}/action
before:DELETE /api/v1/admin/services/{id}/change
before:POST /api/v1/admin/services/{id}/change
before:POST /api/v1/admin/services/{id}/moduleManually (re)run a module action. Does not change the billing status.
before:POST /api/v1/admin/services/{id}/module/custom
before:POST /api/v1/admin/services/{id}/renew-now
before:POST /api/v1/admin/services/{id}/usage
before:PUT /api/v1/admin/settings/add-funds
before:PUT /api/v1/admin/settings/autopay
before:PUT /api/v1/admin/settings/bank-transfer
before:PUT /api/v1/admin/settings/domain-defaults
before:PUT /api/v1/admin/settings/due-reminders
before:PUT /api/v1/admin/settings/einvoice
before:PUT /api/v1/admin/settings/fraud
before:PUT /api/v1/admin/settings/housekeeping
before:PUT /api/v1/admin/settings/inactive-clients
before:PUT /api/v1/admin/settings/invoice-branding
before:PUT /api/v1/admin/settings/on-demand-renewals
before:PUT /api/v1/admin/settings/order-approval
before:PUT /api/v1/admin/settings/overdue
before:PUT /api/v1/admin/settings/proforma
before:PUT /api/v1/admin/settings/registrant-verification
before:PUT /api/v1/admin/settings/reversal
before:PUT /api/v1/admin/settings/smtp
before:POST /api/v1/admin/settings/smtp/testSend a test mail synchronously with the *saved* settings.
before:PUT /api/v1/admin/settings/tax
before:PUT /api/v1/admin/settings/ticket-auto-closeauto close put
before:PUT /api/v1/admin/setup-wizard/dismissed
before:PUT /api/v1/admin/siteadmin put
before:POST /api/v1/admin/site/pagesadmin create
before:DELETE /api/v1/admin/site/pages/{id}admin delete
before:PUT /api/v1/admin/site/pages/{id}admin update
before:PUT /api/v1/admin/support/departments/{id}update department
before:POST /api/v1/admin/support/departments/{id}/pipepipe now
before:POST /api/v1/admin/support/spam-rulesadd spam
before:DELETE /api/v1/admin/support/spam-rules/{id}del spam
before:PUT /api/v1/admin/system/updates
before:POST /api/v1/admin/system/updates/check
before:POST /api/v1/admin/tax-rules
before:DELETE /api/v1/admin/tax-rules/{id}
before:PUT /api/v1/admin/tax-rules/{id}
before:PUT /api/v1/admin/themes/active
before:POST /api/v1/admin/ticket-canned
before:DELETE /api/v1/admin/ticket-canned/{id}
before:PUT /api/v1/admin/ticket-canned/{id}
before:POST /api/v1/admin/ticket-departments
before:PUT /api/v1/admin/ticket-departments/{id}
before:PUT /api/v1/admin/ticket-departments/{id}/sla
before:POST /api/v1/admin/ticket-escalations
before:POST /api/v1/admin/ticket-escalations/runEnqueue an escalation pass now (durable job; the worker also schedules one
before:DELETE /api/v1/admin/ticket-escalations/{id}
before:PUT /api/v1/admin/ticket-escalations/{id}
before:POST /api/v1/admin/ticket-statuses
before:DELETE /api/v1/admin/ticket-statuses/{slug}
before:PUT /api/v1/admin/ticket-statuses/{slug}
before:POST /api/v1/admin/tickets
before:PATCH /api/v1/admin/tickets/{id}
before:POST /api/v1/admin/tickets/{id}/merge
before:DELETE /api/v1/admin/tickets/{id}/messages/{mid}
before:PUT /api/v1/admin/tickets/{id}/messages/{mid}
before:POST /api/v1/admin/tickets/{id}/reply
before:POST /api/v1/admin/tickets/{id}/split
before:PUT /api/v1/admin/tickets/{id}/tags
before:POST /api/v1/admin/tickets/{id}/timetime add
before:DELETE /api/v1/admin/tickets/{id}/time/{entry}time del
before:PUT /api/v1/admin/tickets/{id}/watchersput watchers
before:PUT /api/v1/admin/tlds
before:POST /api/v1/admin/tlds/bulk-adjustadmin tld adjust
before:POST /api/v1/admin/tlds/copy-currencyadmin tld copy
before:DELETE /api/v1/admin/tlds/{tld}/{currency}
before:POST /api/v1/admin/todos
before:DELETE /api/v1/admin/todos/{id}
before:PUT /api/v1/admin/todos/{id}
before:POST /api/v1/admin/tracked-links
before:DELETE /api/v1/admin/tracked-links/{id}
before:PATCH /api/v1/admin/transactions/{id}
before:DELETE /api/v1/admin/translations
before:PUT /api/v1/admin/translations
before:POST /api/v1/admin/usage-metrics
before:DELETE /api/v1/admin/usage-metrics/{id}
before:PUT /api/v1/admin/usage-metrics/{id}
before:POST /api/v1/admin/usage/bill
before:POST /api/v1/admin/usage/sample
before:POST /api/v1/admin/users/{id}/api-keys/revoke-all
before:POST /api/v1/admin/users/{id}/login-aslogin as user
before:DELETE /api/v1/admin/users/{id}/passkeysRevoke every passkey of a user (lost device / compromised account).
before:PUT /api/v1/admin/users/{id}/roles
before:POST /api/v1/admin/users/{id}/verify-emailMark a user's email address as confirmed (staff override). Idempotent.
before:POST /api/v1/admin/vendor-catalog/{id}/import
before:PUT /api/v1/admin/vendor-catalog/{id}/markup
before:POST /api/v1/affiliates/trackSPA landing (/r/:code): validates the code and sets the tracking cookie.
before:POST /api/v1/api-keys
before:DELETE /api/v1/api-keys/{id}
before:DELETE /api/v1/auth/avatar
before:PUT /api/v1/auth/avatar
before:POST /api/v1/auth/duo/finish
before:POST /api/v1/auth/duo/startSwap the password-step challenge for a Duo authorize URL. Health-checks
before:POST /api/v1/auth/email/resend
before:POST /api/v1/auth/email/verify
before:POST /api/v1/auth/impersonation/stopstop impersonation
before:PUT /api/v1/auth/locale
before:POST /api/v1/auth/login
before:POST /api/v1/auth/logout
before:POST /api/v1/auth/mfa/backup-codesReplace all backup codes; returned once.
before:POST /api/v1/auth/mfa/disableTurn 2FA off; needs the password and a current code (or a backup code).
before:POST /api/v1/auth/mfa/enable
before:POST /api/v1/auth/mfa/setupStart enrolment: a fresh pending secret, replaced on each call until enabled.
before:POST /api/v1/auth/mfa/verifySecond login step. Max 5 attempts per challenge, 5 minute lifetime.
before:POST /api/v1/auth/passkeys/discover/finish
before:POST /api/v1/auth/passkeys/discover/startUsernameless sign-in: empty allowCredentials, the authenticator picks a
before:POST /api/v1/auth/passkeys/login/finishPasswordless sign-in. A user-verified passkey counts as both factors.
before:POST /api/v1/auth/passkeys/login/start
before:POST /api/v1/auth/passkeys/reauth/finishStep-up with a passkey instead of password + code.
before:POST /api/v1/auth/passkeys/reauth/start
before:POST /api/v1/auth/passkeys/register/finish
before:POST /api/v1/auth/passkeys/register/startEnrolment needs the password (and 2FA code when enabled) again.
before:DELETE /api/v1/auth/passkeys/{id}
before:POST /api/v1/auth/password/changechange password
before:POST /api/v1/auth/password/forgot
before:POST /api/v1/auth/password/reset
before:POST /api/v1/auth/reauthreauth
before:POST /api/v1/auth/register
before:POST /api/v1/auth/saml/logoutlogout
before:DELETE /api/v1/auth/sessionsSign out every other device; the calling session stays valid.
before:DELETE /api/v1/auth/sessions/{id}
before:POST /api/v1/chat/sessions
before:POST /api/v1/chat/sessions/{id}/close
before:POST /api/v1/chat/sessions/{id}/messages
before:POST /api/v1/client/affiliate
before:POST /api/v1/client/affiliate/payouts
before:PUT /api/v1/client/billing/auto-pay
before:POST /api/v1/client/billing/payment-methods
before:DELETE /api/v1/client/billing/payment-methods/{id}
before:POST /api/v1/client/billing/payment-methods/{id}/default
before:POST /api/v1/client/checkoutTurn a cart into an order + invoice for the signed-in customer. The
before:POST /api/v1/client/contacts
before:DELETE /api/v1/client/contacts/{id}
before:POST /api/v1/client/coupons/checkValidate a code for the order form. Returns only what the customer needs.
before:POST /api/v1/client/credit/add-fundsCreate a deposit invoice; once paid (any gateway) the amount is credited.
before:POST /api/v1/client/domains
before:POST /api/v1/client/domains/bulkclient bulk
before:PUT /api/v1/client/domains/{id}/addonsclient addons
before:PUT /api/v1/client/domains/{id}/auto-renew
before:PUT /api/v1/client/domains/{id}/contact
before:PUT /api/v1/client/domains/{id}/dnsclient dns put
before:POST /api/v1/client/domains/{id}/eppReveal the transfer-out (EPP) code. Audited; owner only, active domains.
before:PUT /api/v1/client/domains/{id}/nameservers
before:POST /api/v1/client/domains/{id}/registrant-verification/resend
before:POST /api/v1/client/domains/{id}/renew
before:PUT /api/v1/client/email-preferences
before:PUT /api/v1/client/fields
before:POST /api/v1/client/gift-cards/redeem
before:POST /api/v1/client/invoices/mass-payPay several of my unpaid invoices from account credit, oldest due first.
before:POST /api/v1/client/invoices/mergeClient-side merge (WHMCS "mass payment"): combine own unpaid invoices into
before:POST /api/v1/client/invoices/{id}/apply-credit
before:POST /api/v1/client/invoices/{id}/payStart paying an invoice with a gateway: redirect (Stripe) or manual
before:POST /api/v1/client/kyc/documentsRaw body upload. PNG/JPEG/WebP/PDF only.
before:DELETE /api/v1/client/kyc/documents/{id}
before:PUT /api/v1/client/marketing
before:POST /api/v1/client/orders
before:POST /api/v1/client/payments/{id}/completePayer returned from the provider: capture/check and apply (idempotent).
before:PUT /api/v1/client/profile
before:POST /api/v1/client/quotes/{id}/accept
before:POST /api/v1/client/quotes/{id}/decline
before:DELETE /api/v1/client/services/{id}/cancellation
before:POST /api/v1/client/services/{id}/cancellation
before:DELETE /api/v1/client/services/{id}/change
before:POST /api/v1/client/services/{id}/change
before:POST /api/v1/client/services/{id}/module/{action}
before:POST /api/v1/client/services/{id}/renew-now
before:POST /api/v1/client/services/{id}/sso
before:POST /api/v1/client/subaccountsCreate a sub-account that signs in with its own email and password and
before:POST /api/v1/client/subaccounts/inviteInvite a contact by email: the account is created with an unusable random
before:DELETE /api/v1/client/subaccounts/{id}Remove a contact: its access ends immediately (sessions revoked) and
before:PUT /api/v1/client/subaccounts/{id}
before:POST /api/v1/client/tickets
before:POST /api/v1/client/tickets/{id}/close
before:POST /api/v1/client/tickets/{id}/rate
before:POST /api/v1/client/tickets/{id}/reply
before:POST /api/v1/client/vat/verify
before:POST /api/v1/compat/whmcshandle
before:POST /api/v1/headless/cartsCreate a server-side cart owned by the key's customer; the response holds
before:PUT /api/v1/headless/carts/{id}
before:POST /api/v1/headless/carts/{id}/checkoutTurn the cart into an order and a finalized invoice (fraud screening,
before:POST /api/v1/headless/invoices/{id}/payStart a payment for an invoice; returns the gateway redirect/action.
before:POST /api/v1/licensing/verifyPublic. Bind on first use, then verify; the response is HMAC-signed with
before:POST /api/v1/oauth/authorizeauthorize decide
before:POST /api/v1/public/contactcontact submit
before:POST /api/v1/public/domains/verify-registrantE-mail link target: confirms the registrant address (all its pending domains).
before:POST /api/v1/public/kb/articles/{slug}/vote
before:POST /api/v1/sso/consumeCalled by the panel. 200 with the claims the first time; 401 otherwise.
before:POST /api/v1/sso/saml/{id}/acsacs
before:PUT /api/v1/store/carts/{id}Create or replace a cart. The id is an unguessable client-generated
before:POST /api/v1/tickets/attachmentsUpload a file (raw body). Returns an id to pass as attachment_ids when
before:POST /api/v1/unsubscribe/{token}No CSRF/session: the unguessable token is the credential.
before:POST /api/v1/webhooks/{gateway}

#Mixins

A mixin wraps one core route under /api/v1 (any method, GET too; not /api/v1/auth/* or addon routes): before rewrites the request body, after rewrites the JSON response (only values of keys it already has), around does both with shared state, and return replaces the response with one of the same shape. Mixins need admin approval (mixins, mixins.return) and can be switched off per addon. See Hooks and mixins.

extensions/examples/addon-guide/src/lib.rs · snippet mixinRust
/// Mixins wrap a core route (`capabilities.mixins`). An `after` mixin may
/// change values of keys the response already has; new keys are dropped.
fn mixin(input: &Value) -> Value {
    let mut resp = input["response"].clone();
    // Opt-in per request so this example never changes other callers.
    let wanted = input["request"]["query"].as_str().unwrap_or("").contains("guide=1");
    if input["at"] == "after" && wanted {
        if let Some(c) = resp["city"].as_str() {
            resp["city"] = json!(c.to_uppercase());
        }
        return json!({ "response": resp });
    }
    json!({})
}