Storage & webhooks

Two things the platform keeps for you so a small app needs no database of its own: a per-install key–value store, and webhook delivery for every workspace that installs you. Both are addressed by the install, so an access token is all you need; neither takes a scope.

App data storage

A settings and state store, keyed by install and mode. A zero-backend app keeps its configuration here; an app with a real backend keeps its own data and uses this for the few things that belong with the install.

GET    /v1/app-data              # list this install's keys (values omitted)
GET    /v1/app-data/:key         # one value
PUT    /v1/app-data/:key         # { "value": <any JSON> } — create or replace
DELETE /v1/app-data/:key
JavaScript
await fetch('https://api.paysio.com/v1/app-data/settings', {
  method: 'PUT',
  headers: { Authorization: 'Bearer ' + accessToken, 'Content-Type': 'application/json' },
  body: JSON.stringify({ value: { emailCustomer: true, template: 'default' } }),
})

const res = await fetch('https://api.paysio.com/v1/app-data/settings', {
  headers: { Authorization: 'Bearer ' + accessToken },
})
const { value } = await res.json()   // { emailCustomer: true, template: 'default' }
RuleDetail
Keys1–128 characters of letters, digits, . _ : -. Up to 500 per install.
ValuesAny JSON, up to 64KB serialised.
ModePart of the primary key. A test-mode token cannot see live rows, and vice versa.
AccessApp tokens only. A merchant API key gets 403 app_required: there is no install for it to address.
LifetimeKept 30 days after uninstall, then purged. A merchant who reinstalls within that window finds their settings intact.
Errorsparameter_invalid for a bad key or oversized value; limit_exceeded at 500 keys; resource_missing on GET/DELETE of an unknown key.

Webhooks

Set webhooks.url in paysio.app.json and we provision an endpoint for every workspace that installs you, signed with your app’s own webhook secret. You get that secret once, on your app’s page in the dashboard. Deliveries are the same shape, signature and retry policy as a merchant’s own webhooks, with two additions: the platform topics, and scope gating.

Verifying a delivery

TypeScript
import crypto from 'node:crypto'

function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))   // t=..., v1=...
  const expected = crypto.createHmac('sha256', secret).update(parts.t + '.' + rawBody).digest('hex')
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300       // 5 minutes
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}

// Express: app.post('/hooks', express.raw({ type: '*/*' }), (req, res) => { ... })
// Verify the RAW body. A parsed-and-reserialised body will not match.

Platform topics

Always on. An app cannot unsubscribe from being told it was removed or that its permissions changed.

TopicWhenDo
app/uninstalledThe merchant removed your app.Delete or anonymise that workspace’s data on your side. Stop any jobs. Tokens are already revoked.
app/scopes_updateThe granted set changed: a grant after requestScope(), a revocation, or re-consent after an approved widening.Read the new granted_scopes and enable or disable features accordingly.

Business topics

A payload is a read: payment.completed carries what GET /transactions/:id would. So each topic is only delivered to apps that declared and were granted its read scope. Declaring a topic without its scope is rejected when you save the config, rather than silently never arriving.

TopicsRequires
payment.completed, payment.refunded, payment.voided, payment.failed, order.fulfilled, order.cancelledtransactions:read
payment.disputed, dispute.created, dispute.updated, dispute.evidence_submitted, dispute.won, dispute.lostdisputes:read
payout.created, payout.updated, payout.paid, payout.failed, payout.returnedpayouts:read
subscription.created, subscription.updated, subscription.cancelled, subscription.paused, subscription.resumed, subscription.renewed, subscription.payment_failed, subscription.trial_endingsubscriptions:read

Delivery

  • Each delivery carries the workspace it belongs to; one endpoint serves every install.
  • Answer 2xx within 10 seconds. Anything else is retried: 8 attempts over about 24 hours with exponential backoff and jitter.
  • Deliveries can arrive more than once and out of order. Key your handling on the event id.
  • Deliveries stop the moment you are uninstalled or suspended.
  • Sandbox events are delivered with mode: "test". Treat them as rehearsals.

The webhook payloads themselves are documented under Webhooks in the core reference; an app receives the same objects a merchant does.