PlatformXeDocs
Get API Key

Custom Events

Tenant-defined events — register a typed schema, emit payloads, and route them through the bus.

The client.events.custom namespace lets tenants register their own typed event shapes with PlatformXe, emit payloads against them, and let the bus route to subscribers (webhooks, workflows, federation peers). Schemas are JSON Schema 2020-12 (OpenAPI 3.1 component schemas accepted as a strict superset). Available in TypeScript, Python, Go, and Terraform.

SDKInstallNamespace
TypeScriptnpm install @caldera/platformxe-sdkclient.events.custom
Pythonpip install platformxeclient.events.custom
Gogo get github.com/calderax/platformxe-goclient.Events.Custom
Terraformsource = "calderax/platformxe"platformxe_custom_event

Plan tiers

TierRegister your ownEmit genericEmit registeredMonthly emitsVersioning
Free1,000
Basicup to 510,000latest only
Proup to 25100,000full semver
Enterpriseunlimitedunlimitedfull semver + federation

Every organisation is auto-seeded with the platform-provided platformxe.generic catch-all event so even Free-tier tenants can experiment with the engine without registering anything first. Use the kind field on the generic payload to route to the correct subscriber.

Quick start — emit the generic event (Free-tier friendly)

await client.events.custom.emit({
  name: 'platformxe.generic',
  payload: {
    kind: 'user.signed_up',
    summary: 'New user joined',
    data: { userId: 'usr_123', plan: 'free' },
  },
});
client.events.custom.emit(
    name="platformxe.generic",
    payload={
        "kind": "user.signed_up",
        "summary": "New user joined",
        "data": {"userId": "usr_123", "plan": "free"},
    },
)
_, err := client.Events.Custom.Emit(platformxe.EmitCustomEventInput{
    Name: "platformxe.generic",
    Payload: map[string]interface{}{
        "kind":    "user.signed_up",
        "summary": "New user joined",
        "data":    map[string]interface{}{"userId": "usr_123", "plan": "free"},
    },
})

Register a typed event (Basic+)

Schemas are immutable per (namespace, name, version). Evolve a shape by registering a new version — bump rules: patch must be byte-equal except for description; minor must be backwards-compatible; major is free-form.

await client.events.custom.register({
  namespace: 'lettings',
  name: 'property.favorited',
  version: '1.0.0',
  status: 'published',
  description: 'Fired when a customer favorites a property',
  payloadSchema: {
    type: 'object',
    properties: {
      propertyId: { type: 'string', maxLength: 64 },
      subjectId:  { type: 'string', maxLength: 64 },
      favoritedAt: { type: 'string', format: 'date-time' },
    },
    required: ['propertyId', 'subjectId'],
    additionalProperties: false,
  },
});
resource "platformxe_custom_event" "property_favorited" {
  namespace = "lettings"
  name      = "property.favorited"
  version   = "1.0.0"
  status    = "published"

  payload_schema = jsonencode({
    type = "object"
    properties = {
      propertyId  = { type = "string", maxLength = 64 }
      subjectId   = { type = "string", maxLength = 64 }
      favoritedAt = { type = "string", format = "date-time" }
    }
    required             = ["propertyId", "subjectId"]
    additionalProperties = false
  })
}

Emit a registered payload

const result = await client.events.custom.emit({
  name: 'lettings.property.favorited',
  payload: {
    propertyId: 'p_123',
    subjectId: 'sub_456',
    favoritedAt: '2026-05-04T10:00:00Z',
  },
  // Optional: pin to a specific version. Omit to route to latest published.
  version: '1.0.0',
});

result.canonicalName; // 'TENANT_CUSTOM:org_x:lettings.property.favorited@1.0.0'
result.status;        // 'queued'

The bus relays under the canonical name; subscribers (webhooks, workflows, federation peers) match by exact string. Idempotency: pass idempotencyKey to dedupe replays — the audit row's unique index prevents duplicate inserts.

Validate a template before persisting (dry-run)

const verdict = await client.events.custom.dryRun({
  namespace: 'lettings',
  name: 'property.favorited',
  version: '1.0.0',
  payloadSchema: { /* … */ },
});
verdict.valid;          // true
verdict.semverBump;     // 'initial' | 'patch' | 'minor' | 'major'
verdict.schemaHash;     // sha-256 hex
verdict.schemaFlavour;  // 'json-schema-2020-12' | 'openapi-3.1'

Used by terraform plan previews and the SDK type-generation workflow to catch schema problems before they reach the registry.

Health & usage

const h = await client.events.custom.health();
h.plan;                      // 'BASIC'
h.registrations.used;        // 4
h.registrations.limit;       // 5 (or null when plan is unlimited)
h.emits.thisMonth;           // 9_812
h.emits.monthlyLimit;        // 10_000 (or null)
h.recentFailures;            // last 10 emits with deliveryStatus='FAILED'

Sub-resources

Two opt-in sub-namespaces extend the base client.events.custom surface:

  • client.events.custom.marketplace(Pro+) publish a registered event for other tenants to fork into their own org. Browsing is open to all plans; publishing and forking are Pro+. See Marketplace.
  • client.events.custom.federation(Enterprise only) push live emit traffic to peer Enterprise orgs through bus-level fan-out. An owner creates a group, invites peers, and declares per-version pushes; the platform re-emits on each accepted peer's canonical name automatically. Since 1.5.0 — Pattern 3: peers can also be arbitrary HTTPS endpoints addressed by URL + HMAC secret, useful when the receiving system isn't on PlatformXe. See Federation.

These are entirely separate flows from client.permissions.federation (the v1.x.x admin permissions cross-app sync) — different concept, different access path.

Reference — namespaces & naming

  • Namespace^[a-z][a-z0-9_-]{1,30}$. Reserved namespaces (identity, fraud, permissions, messaging, webhooks, workflows, auth, platform, platformxe, events, subscriptions, domains, templates, generic) are blocked globally. Tenants can only register under namespaces on their org's allowlist — seeded with the org slug at onboarding; ops grants additional namespaces.
  • Name^[a-z][a-z0-9_.-]{1,60}$. Dots in names are allowed (e.g. property.favorited).
  • Version — strict MAJOR.MINOR.PATCH (no prereleases or build metadata). Single-step bumps only.
  • Schema caps — depth ≤ 5, total properties ≤ 50 (across all nested levels). Payload size ≤ 256 KB.
  • Canonical nameTENANT_CUSTOM:<organizationId>:<namespace>.<name>@<version>. Org id in the bus name guarantees strict isolation between tenants.

Reference — error codes

CodeStatusCause
invalid_namespace400Namespace fails the regex
reserved_namespace400Tried to use a globally-reserved namespace
namespace_not_allowed400Namespace not on this org's allowlist
invalid_name / invalid_version_format400Shape error
cannot_register_generic_event400Tenants cannot self-register platformxe.generic
version_already_exists / version_downgrade / version_invalid_jump400Semver bump issue
patch_must_be_byte_equal400Patch bump changed the schema beyond description
minor_must_be_backwards_compatible400Minor bump is breaking
schema_too_deep / schema_too_wide400DoS guard tripped
invalid_json_schema400Ajv compile error
payload_example_does_not_match_schema400Example doesn't satisfy own schema
registration_quota_exceeded402Plan cap hit
event_not_registered404Emit target not found / not published
event_not_published409Emit target is draft or archived
payload_invalid400Emit payload fails the registered schema
payload_too_large413Payload > 256 KB
chain_depth_exceeded422Workflow chain hit the platform's 50-deep cap
monthly_emit_quota_exceeded402Monthly emit ceiling reached
bus_relay_failed502Audit row written but the bus push errored
cannot_archive_generic_event403Generic catch-all is system-managed

Lifecycle — what fires when

Every state transition emits a platform event so subscribers (drift detection, federation push, audit, billing meter, ops alerts) stay in sync. The full catalog is in docs/platformxe-tenant-custom-events-lifecycle-events.md. At a glance:

  • Registration: CUSTOM_EVENT_REQUESTEDCUSTOM_EVENT_VALIDATEDCUSTOM_EVENT_PERSISTEDCUSTOM_EVENT_MATERIALISEDCUSTOM_EVENT_REGISTERED.
  • Emit: CUSTOM_EVENT_EMIT_REQUESTEDCUSTOM_EVENT_EMIT_RELAYEDCUSTOM_EVENT_EMIT_DELIVERED (or …_DELIVERY_FAILED).
  • Failure: CUSTOM_EVENT_VALIDATION_FAILED / CUSTOM_EVENT_EMIT_REJECTED / CUSTOM_EVENT_QUOTA_EXCEEDED.
  • Infra: CUSTOM_EVENT_GENERIC_SEEDED, CUSTOM_EVENT_INVALIDATED, CUSTOM_EVENT_CHAIN_TRUNCATED.