{
  "language": "en",
  "alternates": {
    "fr": "https://test.each.app/surface/payments/v0.json",
    "en": "https://test.each.app/en/surface/payments/v0.json",
    "markdown_fr": "https://test.each.app/surface/payments/v0.md",
    "markdown_en": "https://test.each.app/en/surface/payments/v0.md"
  },
  "core_enforced": {
    "currencies": [
      "CAD"
    ],
    "max_per_transaction_cents": 50000,
    "max_per_person_day_cents": 100000,
    "max_per_brand_day_cents": 500000,
    "min_cents": 50,
    "test_mode_only": true
  },
  "module": "payments",
  "version": "v0",
  "status": "draft",
  "updated": "2026-10-30",
  "name": "Payments module",
  "summary": "The Payments module lets a person pay a brand on Each: one-time payment, brand subscription or usage fee. Every transaction produces a signed proof anyone can verify. Each: test mode only, no real payments.",
  "kinds": [
    {
      "id": "one_time",
      "text": "One-time payment: one amount, once (e.g. an order, a ticket)."
    },
    {
      "id": "subscription",
      "text": "Brand subscription: a recurring amount (monthly or yearly) a person or a shop pays a brand."
    },
    {
      "id": "usage",
      "text": "Usage fee: an amount based on how a tool is used (e.g. per order placed through the network)."
    }
  ],
  "core": {
    "fr": "Le noyau Each n'est pas modifiable par un module. Un module peut l'étendre, jamais le contourner. Le noyau garde: les clés d'accès (qui confirme), le consentement (rien n'est payé sans confirmation par clé d'accès), les plafonds en dollars et la signature des preuves.",
    "en": "The Each core cannot be modified by a module. A module may extend it, never bypass it. The core owns: passkeys (who confirms), consent (nothing is paid without passkey confirmation), dollar caps and proof signing.",
    "caps": {
      "currency": "CAD",
      "per_transaction": "$500",
      "per_person_per_day": "$1,000",
      "per_brand_per_day": "$5,000",
      "minimum": "$0.50",
      "test_mode_only": true
    },
    "taxes": "Taxes: coming soon",
    "caps_note": "Each caps, enforced by the core before any provider call. A module may ask for lower caps, never higher ones."
  },
  "scopes": [
    {
      "id": "payments:read",
      "text": "Read your payments and their proofs."
    },
    {
      "id": "payments:checkout",
      "text": "Prepare a payment; it never happens without your passkey."
    },
    {
      "id": "payments:subscribe",
      "text": "Prepare a brand subscription; each one is confirmed by your passkey."
    },
    {
      "id": "payments:usage",
      "text": "A brand reports usage fees, within the core caps."
    },
    {
      "id": "payments:refund",
      "text": "A brand refunds a payment (to be defined, see open questions)."
    }
  ],
  "flow": [
    "The person picks a payment (kind, brand, amount).",
    "The core checks the session, currency and caps.",
    "The person confirms with their passkey (consent).",
    "The module creates the payment with the provider (or simulates it in Each).",
    "The provider confirms through a signed webhook; Each checks the signature and ignores duplicates.",
    "Each issues a signed proof of the transaction."
  ],
  "webhooks": {
    "endpoint": "POST /v1/modules/payments/webhook",
    "signature": "Signature header: t={timestamp},v1={hex HMAC-SHA256 of \"{t}.{raw body}\"} with the webhook secret. Rejected if the signature does not match or t is older than 5 minutes.",
    "idempotency": "Every event has a unique id. An id already received answers 200 and does nothing again. Outgoing calls carry an idempotency key (the Each payment id).",
    "not_configured": "Without a webhook secret, the endpoint answers 503 module_not_configured."
  },
  "proof": {
    "type": "each.payment",
    "fr": "Chaque transaction produit une preuve signée (ES256, même clé publique que les preuves de geste: /.well-known/each-keys), en ajout seulement. Vérifiable sur /verifier/{id}.",
    "en": "Every transaction produces a signed proof (ES256, same public key as gesture proofs: /.well-known/each-keys), append-only. Verifiable at /verifier/{id}.",
    "fields": [
      "v",
      "type",
      "id",
      "issuer",
      "environment",
      "issued_at",
      "test",
      "person",
      "brand",
      "payment.kind",
      "payment.amount_cents",
      "payment.currency",
      "payment.interval",
      "payment.provider_mode",
      "statement",
      "confirmation (passkey)",
      "core.version",
      "module.version"
    ]
  },
  "api": [
    "GET /v1/modules/payments/status",
    "POST /v1/modules/payments/simulate/options",
    "POST /v1/modules/payments/simulate",
    "POST /v1/modules/payments/checkout",
    "POST /v1/modules/payments/webhook",
    "GET /v1/proofs/{id}"
  ],
  "configuration": "The provider key is a server secret added by the Each team. It is never entered in a browser. Without a key: \"module not configured\" state. Live keys are refused in Each.",
  "versioning": "Semantic contract version: v0 = draft. Any breaking change creates v1 at a new address; v0 stays readable. To propose a change: copy src/modules/payments/, edit contract.json, open a review.",
  "open_questions": [
    {
      "text": "Who collects the money: Each, the brand, or connected accounts (each brand has its own account, Each takes a share)?"
    },
    {
      "text": "What share for Each, and on which kinds (one-time, subscription, usage)?"
    },
    {
      "text": "Refunds: who decides, within what delay, and does the original proof stay valid (a refund proof is added)?"
    },
    {
      "text": "Currencies: CAD only at first, then which ones internationally?"
    }
  ]
}