Skip to content

Audit events

Every significant action performed by a merchant, staff member, or the system is recorded to an immutable audit log. You can view the audit log in the dashboard under Settings → Audit Trail, or query it programmatically via the auditLogs GraphQL query.

Each audit log entry contains the following fields:

FieldTypeDescription
actorTypestringWho performed the action: merchant, staff, or system
actorIdstringID of the actor (merchant or staff ID)
actionstringThe action identifier (see Actions below)
targetTypestringType of the record the action was performed on. A stored target uses the record’s class name, for example Merchant, Staff, Transaction, Customer, MerchantWallet, Product, Price or Subscription. Invoices, estimates, credit notes and refunds are named from details instead and read Invoice, Estimate, CreditNote or Refund. Null when the action is not about one record. See Targets
targetIdintegerInteger ID of the affected record. Null for invoices, estimates, credit notes and refunds, whose IDs are in details instead
targetLabelstringHuman-readable name of the affected record, e.g. Invoice INV-0042 · Acme Co (ap@acme.com)
detailsJSONAdditional event-specific metadata
ipAddressstringIP address of the request originator (when available)
sourcestringHow the action was triggered (see Sources below)
createdAtISO 8601 datetimeWhen the event was recorded

Invoices, estimates and credit notes have UUID IDs, which do not fit the integer targetId, so their audit entries carry the record’s ID in details instead. Plirin reads it from there when you query the log and fills in targetType and targetLabel for you. This applies to every entry, including ones recorded before targets were named.

details keytargetTypetargetLabel
credit_note_idCreditNoteCredit note CN-0003 · Acme Co (ap@acme.com)
estimate_idEstimateEstimate EST-0007 · Acme Co (ap@acme.com)
refund_referenceRefundRefund RF-ABC123 · Acme Co (ap@acme.com)
invoice_idInvoiceInvoice INV-0042 · Acme Co (ap@acme.com)
  • When an entry carries more than one of these keys, the first in the table wins. A credit-note entry also names its invoice, and an estimate.converted entry names the invoice it produced, but both are about the credit note or estimate.
  • The customer part is the customer’s name and email, or whichever of the two is known, and is left out when neither is. Staff whose role does not include viewing customers see the label without it.
  • When the record has since been deleted, the label is built from what the entry recorded at the time, marked (deleted): Invoice INV-0007 (deleted) · Acme Co (ap@acme.com). An entry too old to have recorded a number falls back to a short ID, such as Invoice 57f8922d.
  • Only your own records are named. Test-mode and live records are both included, since the audit log itself is not split by mode.
  • The merchant auditLogs query has no target filter. Plirin’s internal support tooling can filter the audit log by target type and ID, but only on stored targets, so it cannot find a target derived this way.

So that an entry still says what it was about after the record is gone, entries about one of these records include its number and customer alongside its ID. They are added to the event-specific fields described under each action below, never in place of them.

Entries aboutdetails fields
An invoiceinvoice_id, invoice_number, customer_name, customer_email
An estimateestimate_id, estimate_number, customer_name, customer_email
A credit notecredit_note_id, credit_note_number, invoice_id, invoice_number, customer_name, customer_email (the customer of the credited invoice)

customer_name and customer_email are the values on the invoice or estimate, or its linked customer’s when those are empty, and are null when neither exists.

SourceDescription
dashboardAction was taken via the Plirin web dashboard
sdkAction was triggered via the Plirin API or SDK
systemAction was performed by Plirin’s internal automation (e.g. scheduled jobs, webhooks processing)
webhookAction was triggered by an inbound webhook from a third-party provider
ActionDescription
analytics.exportedAn analytics report was exported. details includes {family, timeframe, row_count, format}.
ActionDescription
api_key.createdA new API key was created
api_key.revokedAn API key was revoked
ActionDescription
auth_rate_limit_exceededToo many authentication attempts triggered a rate-limit block
auth_lockoutFive failed sign-ins for one email address from one network (an IPv4 /24 or IPv6 /64) within 15 minutes refuse that address from that network for 15 minutes; 50 from any networks refuse it everywhere. details.scope says which (network or global); details.email_hash identifies the address without storing it. A wrong current password when changing the login email counts as a failed sign-in for the account’s current address (details.surface is emailChange)
auth_unlockPlirin operations lifted every sign-in lock on an address. details hold the address hash and the operator
admin_login_failedA platform admin sign-in attempt failed (Plirin staff only, never shown to merchants)
admin_login_successA platform admin signed in: a session was issued, after the two-factor code (details.stage mfa or enrollment) or on the password alone where two-factor is not required (stage password). The admin’s last sign-in time is set at the same moment (Plirin staff only)
admin_password_okA platform admin’s password was accepted. Not a sign-in: details.next names the step still to come (mfa or enrollment, or none when the password alone signs in). A security alert is sent when no sign-in follows within 10 minutes (Plirin staff only)
admin_logoutA platform admin signed out, which ends every session of theirs on the server (Plirin staff only)
admin_mfa_enrolledA platform admin set up two-factor sign-in with an authenticator app; every other session of theirs ended (Plirin staff only)
admin_mfa_enrollment_code_issuedA single-use two-factor enrollment code was issued for a platform admin: by an owner creating the admin or resetting their two-factor (details.issued_by), or by Plirin operations on the server (source cli, details.operator). Setting up two-factor needs this code as well as the password; it expires after 24 hours. details.via names the path; the code itself is never recorded (Plirin staff only)
admin_mfa_failedA platform admin entered a wrong two-factor or recovery code. details.stage says where: login, step_up, enrollment, enrollment_code (a wrong enrollment code) or regenerate. A right code for a step already used (the code that just signed the admin in, offered again) is refused as “just used” and is not recorded here (Plirin staff only)
admin_mfa_lockedFive wrong two-factor codes for one platform admin, counted per admin across sign-ins and networks, paused their two-factor for details.lock_seconds (15 minutes, doubling on each repeat up to 24 hours). A correct password does not reset the count (Plirin staff only)
admin_mfa_recovery_code_usedA platform admin used one of their single-use recovery codes instead of an authenticator code. details.remaining is how many are left (Plirin staff only)
admin_mfa_recovery_codes_regeneratedA platform admin replaced their recovery codes; the old ones stopped working (Plirin staff only)
admin_mfa_resetA platform admin’s two-factor sign-in was removed, by an owner or by Plirin operations on the server (source cli); they set it up again at their next sign-in, with the new enrollment code issued alongside (admin_mfa_enrollment_code_issued) (Plirin staff only)
admin_step_upA platform admin confirmed a two-factor code to unlock sensitive admin actions for five minutes (Plirin staff only)
turnstile_failedA dashboard signup, sign-in, password reset or verification resend was refused by the Cloudflare Turnstile check. details hold the form (turnstile_action) and the reason, never the token
merchant_login_failedA merchant login attempt failed
merchant_login_successA merchant successfully logged in
staff_login_failedA staff login attempt failed
staff_login_successA staff member successfully logged in
staff_token_refreshA staff session token was refreshed
password_reset_failedA password reset attempt failed
password_reset_requestedA password reset was requested
password_reset_successA password was successfully reset
email_verification_sentA verification email was sent to the account’s address
email_verifiedThe account’s email address was confirmed
email_verification_failedA verification link was invalid or had expired
ActionDescription
idempotency.conflictA request arrived with an idempotency key that conflicts with a prior request
idempotency.replayA duplicate request was detected and the cached response was replayed
ActionDescription
customer.createdA new customer record was created
customer.updatedA customer record was updated
customer_upsertedA customer was created or updated via upsert
customer.csv_exportedCustomer data was exported as a CSV
customer.csv_importedCustomers were imported from a CSV file
customer.late_fees_exemptedA customer’s “No late fees” switch changed, from the dashboard or a CSV import; details.exempt is the new value and details.previous the old one; a dashboard change that also removed unpaid fees adds removed_fee_count, removed_fee_ids and kept_paid (fees already paid are never touched)
customer.statements_opted_outA customer was opted out of scheduled statements. Recorded only when the setting changes. A statement can still be sent to them by hand. The target is the customer; details is {name, email}
customer.statements_opted_inA customer was opted back in to scheduled statements. Recorded only when the setting changes. The target is the customer; details is {name, email}
ActionDescription
credit_note.createdA credit note was created against an invoice; details.amount is the credit amount and details.origin says why it exists: manual (a draft you created, which has no financial effect until it is issued), refund_in_plirin (made by a refund you recorded in Plirin), refund_in_stripe (made when Plirin picked up a refund from the Stripe dashboard) or chargeback (made by a lost card dispute)
credit_note.issuedA credit note was issued. details.path says how: paid_refund (the invoice was paid, so the credit is returned as an on-chain refund), paid_card_refund (returned to the card through Stripe, once approved) or paid_recorded_refund (a refund you paid outside Plirin, recorded); each links its refund in details.refund_reference. unpaid_balance_reduction means the credit reduced what is still owed (details.new_remaining is the new balance)
credit_note.settledAn issued credit note finished taking effect. details.path is unpaid_balance_reduction (immediately), refund_completed (its on-chain refund completed), card_refund_completed (Stripe confirmed its card refund) or recorded_refund (a recorded refund, settled as it is recorded)
credit_note.voidedA credit note was voided. When a refund took it down, details.path says which: refund_rejected (the refund request was rejected), card_refund_failed (Stripe failed the card refund; details.reason is the failure code) or recorded_refund_voided (the recorded refund was voided; details.reason is the merchant’s reason)
ActionDescription
estimate.createdA new estimate (quote) was created as a draft
estimate.updatedAn estimate was edited; details.revision says whether this was a draft edit (revision 1) or a new revision of a sent estimate
estimate.sentAn estimate was sent to the customer, on first send or a revision resend; details.revision says which
estimate.voidedAn estimate was voided
estimate.expiredA sent estimate passed the end of its expiry date on the merchant’s calendar and was expired automatically; attributed to the merchant with source system
estimate.acceptedThe customer accepted the estimate on the hosted page; details.signer_name carries the typed signature name
estimate.declinedThe customer declined the estimate on the hosted page; details.reason carries their optional reason
estimate.convertedAn accepted estimate was converted to a draft invoice; details.invoice_id links it, and details.converted_by_merchant marks a dashboard “Convert now”
estimate.hosted_page_viewedThe hosted estimate page was opened, logged at most once per visitor per hour
ActionDescription
agent.proposal_createdA document was uploaded to draft an invoice from; details carry the proposal_id, the attachment_id, its content_type and size_bytes
agent.proposal_acceptedThe merchant reviewed a draft and created an invoice from it; details carry the proposal_id and the invoice_id. The invoice’s own invoice.created entry carries source: document_draft, the proposal_id, the model and the prompt_version
agent.proposal_dismissedThe merchant dismissed a draft and its document was deleted; details.refunded is true when the draft was dismissed before it was read, which returns it to the monthly allowance
agent.proposal_retriedThe merchant retried a failed draft, which uses one draft from the monthly allowance
ActionDescription
invoice.createdA new invoice was created. An imported invoice’s entry carries source: import, import_id, kind and original_number. An invoice created already waived (lateFeesWaived: true on GraphQL createInvoice, or late_fees_waived: true on the REST create route) carries late_fees_waived: true; no separate invoice.late_fees_waived entry is written for it
invoice.updatedAn invoice was updated
invoice.sentAn invoice was sent to the customer
invoice.paidAn invoice was marked as paid
invoice.voidedAn invoice was voided
invoice.attachment_addedA file was attached to an invoice or estimate
invoice.attachment_deletedAn attachment was removed
invoice.attachment_sharing_changedAn attachment was shared with, or hidden from, the customer
invoice.late_fee_appliedAn automatic late fee was charged as a new linked invoice; details carry the original invoice_id, the fee_invoice_id, the fee sequence and amount; attributed to the merchant with source system
invoice.late_fee_skippedAn automatic late fee was due but not charged; details.reason is policy_off, plan, customer_exempt, payment_in_flight, below_minimum or processing_error (the fee could not be processed and is retried the next day), and details.cleared says whether the schedule stopped for good or moved one day; attributed to the merchant with source system
invoice.late_fee_removedA merchant or staff member removed a charged late fee: the fee invoice was voided and its ledger line kept as removed (it still counts towards the invoice’s fee cap); details carry the original invoice_id, the fee_invoice_id, the fee sequence, amount and the reason (or null); source dashboard
invoice.late_fees_waivedA merchant or staff member turned “No late fees on this invoice” on or off for a draft; only a real change writes a row, and an invoice created already waived is recorded on its invoice.created entry instead. details carry invoice_id, invoice_number, late_fees_waived (the new value) and previous; source dashboard
invoice.duplicatedAn invoice was duplicated
invoice.partial_payment_receivedA partial payment was received against an invoice
invoice.reminder_sentA payment reminder was manually sent
invoice.reminders_changedInvoice reminder settings were changed
invoice.auto_reminder_sentAn automatic payment reminder was sent
invoice.email_failedAn invoice or reminder email could not be delivered; kind says which, error gives the mail server’s reason
invoice.auto_overdueAn invoice was automatically marked as overdue
invoice.batch_sentA batch of invoices was sent
invoice.batch_voidedA batch of invoices was voided
invoice.import_committedOpen invoices were imported from a CSV or PDF. Details: import_id, kind, file_name, created, skipped, refused, chase_mode
invoice.import_voided”Void this import” voided the import’s untouched invoices. Details: import_id, voided, skipped
invoice.import_chase_changedThe import chase of one invoice was stopped, rescheduled or restarted. Details: invoice_id, change, next_at, every_days
invoice.branding_updatedInvoice branding settings were updated
invoice.csv_exportedInvoice data was exported as a CSV
reconciliation.csv_exportedOn-chain settlement was exported for reconciliation against the ledger. Details record how many rows were unreconciled, since that is the number a support conversation usually starts from.
invoice.hosted_page_viewedA customer viewed the hosted invoice payment page
invoice.milestone_createdA payment milestone was added to an invoice
invoice.milestone_paidA payment milestone was marked as paid
invoice.milestones_removedMilestones were removed from an invoice
invoice.prefix_changedThe invoice number prefix was changed
invoice.recurring_createdA recurring invoice schedule was created, or an existing one was saved again (a save is a full write of the schedule, so this row is the whole of it). details carries invoice_id, period, next_date (the first send date), lead_days (how many days before each send date the copy is created), carry (the resolved choice of what each copy carries, null for an exact copy) and carry_forced_on (the groups asked off and turned back on, empty when the server changed nothing)
invoice.recurring_cancelledA recurring invoice schedule was stopped on purpose (the Recurring page’s Stop button, cancelInvoiceRecurring, or the REST delete route). The copy already sitting in its review window is disarmed and kept as a plain draft, so it is not sent. Voiding the invoice behind a schedule stops it the same way but writes invoice.voided, not this row
invoice.recurring_blockedA recurring schedule stopped generating because the copy settings saved on it are ones this business may no longer use (most often: tax was set not to carry, and the business has since registered for VAT). Written by the system once per invoice, ever, not once per attempt: the row is what stops the five-minute retry from writing a row every five minutes, so it is never written a second time for the same invoice even after the schedule is fixed and later blocked again. Saving the schedule again is what unblocks generation; it does not remove or supersede this row, so read the row as “this invoice was blocked at least once”, and read invoice.recurring_generated to see whether it has generated since. details carries invoice_id, invoice_number, code, reason and the stored carry
invoice.recurring_generatedA new invoice was generated from a recurring schedule, as a draft armed to send on send_date. details carries source_invoice_id, new_invoice_id, new_invoice_number, period, send_date (when this copy goes out), next_date (the slot after it), and what the schedule said to do: lead_days, carry (null for an exact copy) and anchor_day. When this schedule has drafts that are currently DISARMED (no scheduled_send_at), details.superseded_invoice_ids lists them, most recently created first, up to 20; the key is absent when there are none. Read it as a state and not an event: a disarmed draft is listed again on every later generation, and the row does not establish WHY it is disarmed. A stop or a void disarms the copy in the review window, and cancelScheduledSend disarms one by hand, and the two are indistinguishable on the row. Informational, not a list to act on destructively: every draft in it is one the merchant was deliberately left to send or delete themselves
invoice.recurring_pausedA recurring schedule was paused because one of its copies ran out of send attempts. Written by the scheduled-send worker when it gives up on that copy, once per pause: while a schedule is already paused a second copy of it giving up writes nothing, and a new row is only possible after the merchant has set the schedule up again. details.reason is send_failed, and details also carries source_invoice_id (the schedule), invoice_id and invoice_number (the copy that failed) and attempts. A paused schedule stays listed on the Recurring page, showing as paused, and generates nothing until it is set up again. Copies that pile up unsent never pause a schedule: a hold writes invoice.recurring_held and generation continues
invoice.recurring_heldA generated copy was not sent automatically. The schedule is unaffected and still generating, and the copy is still a draft you can send yourself at any time. details.reason is plan (your plan does not include sending recurring invoices automatically) or outside_paid_period (its send date is earlier than the subscription period you are currently paying for, so releasing it would send a backlog at once). A copy for a day that had already passed is not held at all: it is never given a send date, so it is an ordinary draft and this event does not apply to it. Written once per invoice per reason, not once per attempt, so a merchant who upgrades and is still held gets a second row with the second reason. details also carries invoice_id, invoice_number, source_invoice_id and scheduled_send_at. Three further reason values exist defensively, not_armed, no_merchant and no_period, and none is reachable through the worker’s own query: treat any of them appearing as a fault to report rather than something to act on
invoice.due_date_reanchoredA recurring copy went out on a different day from the one it was generated for, so its due date was re-measured from the day it actually went out, keeping the same payment terms. details carries invoice_id, invoice_number, source_invoice_id, net_days (the terms preserved), and two kinds of date that are not interchangeable: planned_send_day, actual_send_day, due_day_before and due_day_after are days on the merchant’s own calendar, which is what you read to show a date, while due_date_before and due_date_after are the stored instants, which encode the end of the due day and so fall on the next UTC date for a merchant west of UTC. Read the instants only to reconcile against the invoice’s own due_date. Nothing is written when a copy goes out on its planned day, which is the normal case
invoice.payment_recordedAn offline payment (bank transfer, cash, card, check, other) was recorded against an invoice
invoice.payment_removedAn offline payment was removed from an invoice
invoice.payment_restoredA previously-removed offline payment was restored
invoice.payment_correction_notice_failedThe owner’s email about a staff member removing or restoring a payment failed twice; written by the system so support can tell the owner
invoice.refund_recordedA refund the merchant made outside Plirin was recorded against an offline payment, so no money moved through Plirin. details carries refund_reference, payment_id, amount, method, reference, refunded_at and, when there is one, credit_note_id; details.overpayment is true when it returned an overpayment (no credit note)
invoice.refund_recorded_voidedA recorded refund was voided, with its credit note; details.reason says why
late_fee_policy.updatedThe automatic late-fee policy was saved; details.before is the previous policy (null the first time) and details.after the new one
ActionDescription
statement.emailedA customer statement was emailed on request, from the dashboard or the API. The actor is the owner, the staff member or the API key (shown as “API key”). The target is the customer; details includes {number, period_start, period_end, currency, closing_balance, recipient_count, trigger}
statement.scheduled_sentA scheduled customer statement was emailed. Recorded as a system action, shown as “Plirin (scheduled)”. The target is the customer; details has the same fields as statement.emailed
statement.send_failedA statement email could not be delivered. The target is the customer; details includes {number, period_start, period_end, currency, trigger, reason, attempt}, where attempt is the record’s attempt count (1 for the first try, 2 for the retry; a scheduled record stuck mid-send and closed without a retry keeps the count it had). A row is written on every failed attempt. Recorded as the person or API key that sent it for an on-demand send, and as a system action for a scheduled one. A failed on-demand send keeps its number, and sending again takes the next one; a scheduled send is retried once, on the same record
statement.downloadedA live statement PDF was downloaded for a period, from the dashboard or the API. The actor is the owner, the staff member or the API key. Re-downloading a statement that was already sent is not recorded. The target is the customer; details includes {period_start, period_end, currency, closing_balance}
statement.allowance_reachedThe month’s scheduled-statement allowance ran out; the remaining customers were skipped until next month. Recorded as a system action, at most once per merchant per month (the merchant’s own calendar month). The target is the merchant; details is {month, allowance}, for example {"month": "2026-10", "allowance": 200}
statement.settings_changedThe scheduled-statement settings changed. Recorded only when a value actually changed. The target is the merchant; details is {before, after}, each {mode, frequency, day_of_month, weekday, recipients, default_note, scheduled_since}. scheduled_since is the merchant-local date (YYYY-MM-DD) the current schedule started or last moved, or null before a schedule has been saved
ActionDescription
kyb_initiatedKYB verification was initiated
kyb_submittedKYB documents and information were submitted
kyb_approvedKYB verification was approved
kyb_rejectedKYB verification was rejected
kyb_requires_resubmissionKYB was flagged as requiring resubmission
kyb_status_refreshedKYB status was refreshed from the provider
kyb_reconciliation_fixedA KYB reconciliation discrepancy was corrected
kyb_perpetual_event_deferredA KYB perpetual monitoring event was deferred
kyb_webhook_invalid_sigAn inbound KYB webhook had an invalid signature
kyb_webhook_parse_failedAn inbound KYB webhook payload failed to parse
kyb_webhook_unknown_event_kindAn inbound KYB webhook contained an unrecognised event type
kyb_webhook_unknown_merchantAn inbound KYB webhook referenced an unknown merchant
ActionDescription
kyb.aml.hard_hitAn AML hard-hit check was performed
kyb.aml.review_decisionA manual AML review decision was recorded
kyb.aml.screenedA subject was screened against AML lists
kyb.approval.heldA verification approval was withheld because AML or wallet-screening evidence was missing or adverse
kyb.aml.rescreen_requestedAn operator cleared recorded AML verdicts so the merchant is screened again; the previous result is kept in the event details
kyb.consent.givenThe merchant gave consent for KYB processing
kyb.docs.archivedKYB documents were archived
kyb.docs.backup_failedA KYB document backup attempt failed
kyb.docs.backup_startedA KYB document backup was started
kyb.gdpr.erasure_executedA GDPR erasure request was executed for KYB data
kyb.link.refreshedA KYB verification link was refreshed
kyb.provider.requestA request was sent to the KYB provider
kyb.provider.responseA response was received from the KYB provider
kyb.reconciliation.tickA scheduled KYB reconciliation check ran
kyb.tier.fully_verifiedThe merchant reached full KYB verification tier
kyb.tier.upgrade_failedA KYB tier upgrade attempt failed
kyb.tier.upgrade_startedA KYB tier upgrade was initiated
kyb.tier.verifiedA KYB tier verification completed successfully
kyb.vendor.outageA KYB vendor outage was detected
kyb.wallet.sanctionedA wallet was found on a sanctions list during KYB screening
kyb.wallet.screenedA wallet was screened during KYB processing
ActionDescription
merchant_createdA new merchant account was created
merchant_profile_updatedThe merchant’s business profile was updated
merchant_email_changedThe account’s login email was changed, after the current password was confirmed. Verification starts again for the new address, existing sessions end, and the old address is told. details hold from and to
merchant_mainnet_enabledMainnet (live) mode was enabled for the merchant
merchant_mainnet_disabledMainnet (live) mode was disabled for the merchant
merchant.test_mode.enabledThe dashboard was switched to test mode. details includes {merchant_id, from_mode, to_mode}. Only actual transitions are recorded, so re-selecting the mode already in use writes nothing.
merchant.test_mode.disabledThe dashboard was switched to live mode, meaning mainnet payments are no longer refused. Requires an approved KYB verification. details includes {merchant_id, from_mode, to_mode}.
merchant.currency_changedThe account’s default invoicing currency was changed. details includes {field, old_value, new_value}. Written only on an actual change, and in addition to the generic merchant_profile_updated entry, because every invoice issued afterwards is denominated differently — which is the question a reconciliation asks months later. Supported currencies are USD, EUR, GBP, AUD, CAD, CHF, NZD and SGD; only USD and EUR can be settled on-chain.
merchant.ai_features_changedThe account owner turned AI features (drafting invoices from documents) on or off. details carry before and after
merchant.tax_settings_changedA setting that changes what future invoices call their tax, the registration number they print, or the rate that taxes them: business country, flat tax rate, tax ID, tax name or tax ID label override, or default tax behaviour. details includes changes (a list of {field, old_value, new_value}, one per field that actually changed) and the resolved tax name and ID label before and after. Written in addition to merchant_profile_updated, and only when at least one of these fields changed. Invoices already issued keep the wording they were issued with.
tax_rate.createdA tax rate (for example “Standard 19%”) was added. details includes {vat_rate_id, name, rate, kind, is_default, exemption_note}. rate is a decimal fraction as a string ("0.19000").
tax_rate.updatedA tax rate was edited. details includes vat_rate_id and changes, a list of {field, old_value, new_value} for the fields that actually changed. Saving a rate unchanged writes nothing. Invoices already issued keep the rate they were issued at.
tax_rate.deactivatedA tax rate was retired. It stops appearing for new invoices but is kept for the invoices that used it. details includes {vat_rate_id, name, rate}.
tax_rate.default_changedA different tax rate became the default for new invoice lines. details includes {old_default_id, new_default_id}; old_default_id is null when there was no default. Re-selecting the current default writes nothing.
merchant.track_selectedThe merchant selected an onboarding track (e.g. invoicing). details includes {track, plan_code}. Deprecated: new accounts start on invoicing and enable payments through the add-on below, so this is only written by older clients.
merchant.payments_addon_enabledThe stablecoin payments add-on was turned on, moving the account to a plan that grants payments. details includes {from_plan_code, to_plan_code, enabled}. Owner-only, and denied to API keys. Nothing is written when payments is already on, so one entry always means one real change.
merchant.payments_addon_disabledThe stablecoin payments add-on was turned off, returning the account to its invoicing plan. Invoicing features are unaffected. details includes {from_plan_code, to_plan_code, enabled}.
merchant_wallet_addedA wallet address was added to the merchant account
merchant_wallet_removedA wallet address was removed from the merchant account
merchant_wallet_updatedA wallet address on the merchant account was updated
ActionDescription
network.sender_acceptedA merchant accepted a sender in their bills inbox, so that sender’s bills (including earlier ones) now appear there. Written when the member presses Accept on a pending sender, presses Unblock on a blocked sender in Manage senders, and when a signed-in member saves a bill to their inbox from its page. details includes {sender_merchant_id, previous_status, status}. status is accepted; previous_status is what the decision replaced: null for a first decision, blocked when the member had blocked this sender and an unblock (from Manage senders, or saving a bill) lifted the block, or accepted on a repeat. A repeat writes another entry but does not change the stored decision date.
network.sender_blockedA merchant blocked a sender in their bills inbox, from a pending card or from Manage senders. The sender’s bills stay hidden and the sender is not told. details includes {sender_merchant_id, previous_status, status}, where status is blocked and previous_status is what the decision replaced (null, accepted, or blocked on a repeat).
merchant.signed_up_from_billA merchant account was created from the sign-up link emailed to a bill’s address (from the bill page’s Save to a free Plirin inbox). The account is marked email-verified at once and the bill’s sender is accepted, as part of the same step. No separate network.sender_accepted entry is written for that sender. details includes {jti, invoice_id, sender_merchant_id}: the jti identifies the single-use sign-up token that was redeemed, invoice_id the bill it was issued for. The token itself is never recorded.

Viewing the inbox, opening a bill and the new-bill check are not audit events: they write nothing.

ActionDescription
payment_createdA new payment record was created
payment_confirmedA payment was confirmed on-chain
payment_expiredA payment expired without being completed
payment_revertedA payment that had been confirmed was demoted because the chain contradicted it — the on-chain re-check reported that the money never moved. details includes {transaction_id, reference, previous_status, new_status, evidence, revenue_corrections_written}. Recorded with actorType: system: no human triggers this, and it is the only record of why a settled payment stopped being settled. Fires only on a positive contradiction from the chain, never on an unreachable RPC
payment_transaction_servedA customer’s wallet was handed a signable transaction for this payment. details includes {payer_wallet, amount, merchant_amount, fee_amount, fee_recipient}. Recorded with actorType: system, since the payer is not a Plirin account
payment_fee_waivedA payment was served with no platform fee collected despite a non-zero pricing tier. Currently reachable only on EVM, where a wallet that reports it cannot execute an atomic call batch gets a single, full-amount transfer instead of a two-call batch that could land half-executed. details includes {payer_wallet, reason, atomic_capability, capability_source} — reason matches the transaction’s feeWaiverReason, and capability_source: "client_asserted" marks atomic_capability as a claim reported by the customer’s wallet, not something Plirin verified. Recorded with actorType: system, since the payer is not a Plirin account
test_payment.simulatedA test-mode payment was settled by the simulateTestPayment mutation, with no money moving. Every simulated payment is audited: a first-class event, not a backdoor. details includes {reference, amount, transaction_id}, plus the invoice fields listed under Record fields in details when the payment settled an invoice. Only reachable in test mode, against a testnet transaction the merchant owns that is still pending; the settled transaction is permanently marked simulated: true
refund.requestedA refund was requested for a payment. When it was requested from an invoice, details.rail is stablecoin or card (a recorded refund is complete as it is recorded, so it is audited as invoice.refund_recorded instead) and details.path is credit_note (it returns invoice value, with a credit note) or overpayment (it returns money beyond the invoice total, with none)
refund.approvedA refund request was approved
refund.rejectedA refund request was rejected
refund.completedA refund was completed: on-chain, at Stripe for a card refund, or recorded. A card refund’s details carry rail and provider_refund_id (Stripe’s refund ID) and its credit_note_id
refund.failedA card refund failed at Stripe, at once or after first succeeding, or the sweep found a refund with an unknown outcome was never made while the same money had since been refunded in Stripe (CREDIT_OVER_COMMITTED), or an operator gave up on one Stripe never made after repeated attempts (STRIPE_NEVER_CONFIRMED, source admin): the refund is failed, its credit note void and the balance restored (failure_code in the details)
refund.recorded_from_stripeA refund made in the merchant’s Stripe dashboard was recorded from Stripe’s webhook, with its credit note (origin refund_in_stripe) when it returned invoice value
refund.stripe_credit_shortfallA refund made in Stripe did not fit the invoice’s credit notes. It is recorded in full and credited up to what settled credit leaves, so the invoice’s balance is unchanged by it (a paid invoice still owes nothing). The details give credited, over_committed (unsettled credit notes now exceed what is left to credit; the merchant is emailed to void or reject the one describing the same money) and uncredited (the part beyond what the invoice still claimed, with no credit note)
refund.receipt_sentA refund receipt was emailed to the customer
transaction.issue.reportedA merchant reported a problem with one of their own transactions from the dashboard. details includes {reference, category, description, transaction_status, amount}, where category is one of payment_not_received, wrong_amount, duplicate_charge, customer_dispute, other. This row is written before the notification email to support is attempted, and is committed regardless of whether that email succeeds — so a report survives an SMTP outage or a spam filter, and support can still find it here. Seeing this action with no corresponding support thread means the email was lost, not the report
support.message.sentA merchant sent a message from the dashboard’s Support page. details includes {subject, message, reply_to, stated_contact}. Same durability contract as transaction.issue.reported: the row is committed before the email is attempted, so it is the record of record. reply_to is always the authenticated actor’s address, never a value from the request body, so support cannot be directed to reply to an address the sender does not control. Staff get their own address here rather than the account owner’s. stated_contact holds an alternative address supplied on the form, if any: it is recorded and shown to support marked unverified, and never replaces reply_to. Note that details retains the full message text, so this row falls under the same handling as any other customer content
ActionDescription
price_createdA new price was created
price_updatedA price was updated
price_deletedA price was deleted
pricing_syncedPricing was synced across products
product_createdA new product was created
product_updatedA product was updated
product_deletedA product was deleted
product.created_from_invoiceA product was automatically created from an invoice line item
product.csv_exportedProduct data was exported as a CSV
product.csv_importedProducts were imported from a CSV file
product.inventory_enabledInventory tracking was enabled for a product
product.inventory_disabledInventory tracking was disabled for a product
product.stock_adjustedA product’s stock level was manually adjusted
product_image.uploadedAn image was uploaded for a product
product_image.deletedAn image was deleted from a product
product_image.reorderedProduct images were reordered
ActionDescription
platform.subscription.createdA Plirin platform subscription was created
platform.subscription.updatedA Plirin platform subscription was updated
platform.subscription.canceledA Plirin platform subscription was canceled
platform.subscription.downgradedA Plirin platform subscription was downgraded
platform.invoice.createdA Plirin platform billing invoice was created
platform.invoice.paidA Plirin platform billing invoice was paid
platform.payment.duplicate_receivedA verified second stablecoin payment arrived for a Plirin platform billing invoice that was already paid. It is not credited automatically; support refunds or credits it. Details carry the transaction hash, verified amount and paying wallet
platform.payment.rejectedA stablecoin payment for a Plirin platform billing invoice was refused (not found on-chain, wrong recipient, token or amount, credited to a token account other than Plirin’s payment account, transaction already used, or invoice already paid). The invoice stays unpaid; details carry the reason and the submitted transaction hash
platform.payment.after_voidA successful card payment arrived for a Plirin platform billing invoice that is void or uncollectible. Nothing is settled; support refunds it by hand. details hold the merchant, the invoice and its status, the amount and currency, the provider and its payment reference, never card data (Plirin staff only)
platform.invoice.voidedA Plirin platform billing invoice was voided by a Plirin owner (Plirin staff only, never shown to merchants). details hold the reason, the previous_status (open or paid) and, when a paid invoice’s revenue was reversed, reversed_entry_id and revenue_reversal_entry_id. open_card_checkout is true when an open invoice had a card checkout started (it may still be payable at the provider), and credit_applied is the credit the invoice spent (its whole amount when it was settled from credit) or unknown. The merchant’s plan and credit are not changed by a void
platform.invoice.uncollectibleA Plirin platform billing invoice was marked uncollectible
platform.billing.checkout.createdA card checkout was opened for a platform billing invoice
platform.billing.webhook.invalid_sigA payment webhook arrived with an invalid signature and was rejected
platform.billing.unattributedA card payment succeeded but could not be matched to an invoice — needs manual review
platform.plan.overriddenA Plirin platform plan was manually overridden
platform.credit.appliedCredits were applied to a Plirin platform account
platform.grace.extendedThe grace period was extended for a Plirin platform account
ActionDescription
subscription_createdA merchant subscription was created, either billing immediately (createSubscription) or as a checkout link a customer completes themselves (createSubscriptionLink, details carry checkout_slug)
subscription_updatedA merchant subscription was updated
subscription_canceledA merchant subscription was canceled
subscription_pausedA merchant subscription was paused
subscription_resumedA paused or on-hold subscription was resumed
subscription_renewedA subscription was renewed for a new billing period
subscription_plan_changedA subscription moved to a different price mid-cycle. Details carry the previous and new price, the proration behaviour, and the credit, charge and net the change produced
subscription_past_dueA subscription entered past-due status
subscription_activatedA subscription moved to active: either a trialing subscription’s trial ended and its first invoice was collected, or a checkout link’s connecting wallet cleared screening and the subscription went live as trialing or active
subscription_on_holdA subscription exhausted its dunning retries and stopped collecting
subscription_charge_failedA subscription’s billing attempt for a period failed, whatever the cause: the prior period’s invoice went unpaid past its due date, minting or sending failed, or an on-chain collection attempt failed. The resulting status (past_due or on_hold) is recorded in the entry’s own details rather than as a separate action
subscription_collection_stuckAn automatic collection attempt’s outcome could not be determined (no broadcast signature was ever recorded, or the chain lookup itself kept failing) and requires operator reconciliation; the subscription is not dunned for this
subscription_keeper_gas_lowThe subscription keeper’s native gas balance fell below its configured warning floor on a chain
subscription_keeper_unavailableThe subscription keeper could not load a signing identity (absent or malformed key), so every automatic subscription in that mode degraded to send_invoice for the tick
subscription_mandate_insufficientA mandate could not collect a period automatically: either its real charge (converted into the mandate’s collection token) exceeded the mandate’s authorized per-period cap, or the merchant’s receiving wallet changed since the mandate’s last confirmed collection. Either way that period is degraded to a sent invoice instead of failing or cancelling; details.reason names which
subscription_mandate_authorizedA pending mandate was confirmed on chain and promoted to active, either from the customer’s initial authorization or a re-authorization that supersedes an existing one
subscription_mandate_revokedA mandate can no longer collect: the customer revoked it on chain (or another application overwrote the Solana delegate), or a merchant staff member revoked it directly. The subscription falls back to sending invoices rather than being cancelled. A merchant-initiated revocation only stops Plirin from attempting to collect; on Solana, Plirin holds the SPL delegate and cannot revoke itself, so the customer’s wallet keeps a live delegated allowance to Plirin’s keeper until the customer revokes it themselves on chain. On a shared Solana token account this fires once per affected mandate: a merchant-side revoke reaches that merchant’s own mandates only, while a customer-side revocation really does end the delegation for everyone on the account and can therefore span more than one merchant. A mandate revoked as collateral either way is attributed to no actor with a system source, since nobody clicked revoke on that subscription. A merchant revocation through the dashboard carries the acting staff member and a dashboard source; the same call through the REST API or an SDK carries no actor and an sdk source, matching cancel/pause/resume. Revoking an already-revoked mandate is an idempotent no-op and writes no second entry
subscription_mandate_exhaustedA mandate’s periods_consumed reached its authorized periods_authorized; it can no longer collect until the customer re-authorizes
subscription_mandate_supersededAn active mandate was replaced by a new one from a re-authorization, rather than mutated in place, so the prior mandate’s history survives for a dispute
ActionDescription
stripe.connectedThe merchant connected their own Stripe account to accept card payments
stripe.reconnectedA connection that needed attention was re-authorised
stripe.disconnectedThe Stripe connection ended: the merchant disconnected it, it was revoked from the Stripe dashboard, or it was replaced by another account. details names the reason
stripe.needs_reconnectPlirin moved to a new Stripe platform account, so a connection the previous one approved can no longer take card payments. Card payments pause for that merchant only and the owner is emailed once to reconnect; details carry the stripe_account_id, the old_platform_account_id and the new platform_account_id. Source system
stripe.account_restrictedStripe reported that the connected account can no longer accept charges, so card payments stop being offered until it is resolved
stripe.webhook_invalid_signatureA request to the Stripe webhook endpoint failed signature verification and was discarded
merchant_card_payments_beta_updatedPlirin staff turned early access to card payments on or off for the merchant. Only meaningful while the platform switch is set to beta
merchant_beta_feature_updatedPlirin staff added the merchant to, or removed it from, the beta of a feature (such as customer statements). details names the flag, whether it is now enabled, and the previous value. Only meaningful while that feature’s platform switch is set to beta
invoice.paid_by_cardA card payment succeeded against an invoice. details carries the amount, currency, Stripe PaymentIntent id, whether it was live or test mode, and Plirin’s fee
invoice.card_payment_needs_refundA card payment succeeded that the invoice did not need: the invoice was void, or the payment took it past its total. The money is in the merchant’s Stripe account and should be refunded. details.reason is void_invoice or overpaid
invoice.card_payment_disputedA client disputed a card payment, or their bank sent an inquiry about it (details.dispute_status is the status when first seen: warning for an inquiry, inquiry_closed for an inquiry first seen already closed, which was never a chargeback and needs no response, or needs_response or under_review for a dispute, or won or lost for a dispute first seen already decided). Recorded once per dispute. While it is still open (warning, needs_response or under_review) the merchant is emailed, with the deadline to respond in Stripe; a closed inquiry or an already decided dispute sends no email. The invoice stays paid while the dispute is open. details carries the dispute id, amount, Stripe’s reason code and whether it was live or test mode
invoice.card_payment_dispute_lostThe merchant lost a card dispute: the client kept the disputed amount. A settled chargeback credit note credits it (details.credit_note_id, and credited, its amount), so the invoice stays whole. Recorded once per dispute
invoice.card_payment_dispute_wonThe merchant won a card dispute. Nothing on the invoice changes
invoice.card_payment_dispute_credit_shortfallA lost card dispute did not fit the invoice’s credit notes. It is credited up to what settled credit leaves, so the invoice’s balance stays whole. The details give credited, uncredited (the part already credited before, usually by a refund of the same payment, which Stripe has now taken again) and over_committed (unsettled credit notes now exceed what is left to credit); the merchant is emailed
stripe.payment_unknown_statusStripe reported a payment status Plirin does not recognise, so the payment was left unchanged and will be read again
stripe.payments_unreachableThe merchant’s Stripe account was disconnected (by the merchant, or from Stripe’s dashboard) while card payments on it were still open. They are left as they are, because Plirin can no longer read or cancel them and money may still move; check the Stripe dashboard and record any payment that lands by hand. details.payment_ids lists them
stripe.payment_succeeded_unreachableStripe reported a card payment as succeeded on an account Plirin can no longer reach. It was not recorded, because it cannot be verified; reconcile it in the Stripe dashboard and record it on the invoice by hand. details names the invoice and the Stripe PaymentIntent
invoice.card_payments_changedCard payment was turned on, off or back to the merchant’s default for one invoice. Turning it off cancels the invoice’s open card payment. details carries card_payments_allowed (true, false or null for the default) and previous
merchant.card_payments_default_changedThe owner turned the merchant-wide card payment default on or off. Invoices with their own setting keep it. details carries card_payments_default and previous
invoice.double_payment_suspectedAn on-chain payment settled an invoice while a card payment for it was still processing at Stripe. Both may land, so check the invoice and refund the card payment if it succeeds. details names the transaction and the Stripe PaymentIntent
ActionDescription
staff_createdA new staff member was created
staff_updatedA staff member’s details were updated
staff_deactivatedA staff member was deactivated
staff_role_assignedA role was assigned to a staff member
staff_role_removedA role was removed from a staff member
ActionDescription
webhook_endpoint_createdA webhook endpoint was created
webhook_endpoint_updatedA webhook endpoint was updated
webhook_endpoint_deletedA webhook endpoint was deleted
webhook_secret_rotatedA webhook signing secret was rotated
webhook_replayedA webhook delivery was manually replayed
webhook_exhaustedA webhook endpoint exhausted all retry attempts
ActionDescription
wallet.newly_flaggedA wallet was newly flagged during screening
wallet.rescreening.tickA scheduled wallet rescreening check ran

These are the audit trail for sanctions screening. They are written whether or not screening results in a hit, because the absence of a check is itself a fact a reviewer needs.

ActionDescription
payer_wallet_sanctioned_hitA paying wallet matched the sanctions list. details includes {wallet, chain, case_id}. The payment is held, the wallet is denylisted, and a compliance case is opened in the same step
payer_unscreenedA payment could not be screened because the paying wallet was not known. details includes {context, reason}. Recorded rather than passed over silently, so an unscreened payment is never mistaken for a cleared one
compliance_case_openedA compliance case was opened for a sanctions hit. details includes {context}, the point in the flow where the hit was detected
compliance_case_confirmedAn administrator confirmed the case as a true match
compliance_case_dismissedAn administrator dismissed the case as a false positive. details includes {reason}
compliance_case_reportedThe case was reported to the relevant authority. details includes {reference}, the filing reference
sanctioned_wallet_addedA wallet was added to the denylist, either by an administrator or automatically on a confirmed hit. details includes {address, chain}
sanctioned_wallet_removedA wallet was removed from the denylist by an administrator. details includes {address, chain}
sdn_list_refreshedThe sanctions list was refreshed from its source. details includes {upserted}, the number of entries written