PayOpsDocs

Lifecycle events

Consume stable payment lifecycle events

Use a small, versioned event contract for paid invoices and payment exceptions, with canonical bytes and replay-safe identifiers.

01

Why lifecycle events exist

Your product, finance system, CRM, and reporting jobs should not independently inspect Solana transactions and reach different conclusions. PayOps emits one versioned event after the authoritative decision is durably stored.

Consumers receive business facts rather than raw chain interpretation. They can update an order, create an accounting entry, notify a customer, or open a review task without reimplementing reconciliation.

02

Invoice paid

invoice.paid.v1 is emitted only after a finalized transfer exactly matches the expected mint, recipient, amount, and invoice reference.

The envelope carries a stable event ID, occurrence time, source identity and version, invoice object, and the evidence fields needed to trace the decision. Treat the event as a statement about the persisted invoice state, not a request to recalculate it.

Example
{
  "schemaVersion": "0.1",
  "type": "invoice.paid.v1",
  "object": { "type": "invoice", "id": "inv_0421" }
}
03

Payment exception

payment.exception.v1 carries the review reason and the evidence needed to investigate. An exception never silently mutates invoice state.

The payload includes transfer signature and instruction coordinates, exact amount, review state, classification, code, and rule version. A consumer can route the exception without fetching or guessing the missing context.

04

Verify the envelope

Verify the signature against the exact raw bytes first. Then validate the schema version, exact key sets, UUIDs, timestamps, event type and object pairing, and event-specific data before executing a side effect.

During secret rotation, accept signatures from the current and previous secret for a short overlap. Never log either secret, the signature input, or an unbounded attacker-controlled error value.

05

Replay safely

Deduplicate by event ID, persist before responding, and make downstream handlers idempotent. Manual replay preserves the same event and canonical payload bytes.

If the side effect fails, leave the event unprocessed so the delivery can retry. If it succeeds, commit the processed event ID in the same local transaction as the side effect whenever your storage model allows it.

06

Handle delivery outcomes

Return 2xx only after durable acceptance. Authentication or schema failures should return a terminal 4xx. Temporary network failures, rate limits, and server errors can be retried within the bounded policy.

Operators can inspect attempt history without reading webhook secrets or dumping full payloads. A dead delivery remains available for investigation and an intentional one-shot manual replay.