{
  "title": "Tempest Pay developer integration guide",
  "provider": "Tempest Pay",
  "version": "1.0",
  "docs": {
    "human": "https://tempesttouchwallet.com/docs/integration",
    "sandbox": "https://tempesttouchwallet.com/docs/sandbox.html",
    "examples": "https://tempesttouchwallet.com/docs/examples.html",
    "migration": "https://tempesttouchwallet.com/docs/migration.html",
    "json": "https://tempesttouchwallet.com/docs/integration-spec.json",
    "openapi": "https://tempesttouchwallet.com/docs/openapi.json",
    "postman": "https://tempesttouchwallet.com/docs/postman.tempest-pay.json",
    "llms": "https://tempesttouchwallet.com/.well-known/llms.txt"
  },
  "baseUrl": "https://tempesttouchwallet.com/api/v1",
  "auth": {
    "type": "bearer",
    "header": "Authorization: Bearer <secret_key>",
    "secretNotes": [
      "Keep the secret key on your backend only.",
      "Never expose it in a browser or mobile app.",
      "Use a separate test key for sandbox testing and a live key only in production."
    ],
    "webhookSecretName": "TEMPEST_WEBHOOK_SECRET",
    "webhookNotes": [
      "Read the raw request body before parsing JSON.",
      "Verify Tempest-Signature against the raw body.",
      "Use the event id to make webhook handling idempotent."
    ]
  },
  "modes": [
    "test",
    "live"
  ],
  "requiredFlow": {
    "summary": "Create a charge on your backend, redirect the customer to hosted_url, then return them to the configured success or cancel URL with payment outcome details. Fulfil orders only after the signed charge.paid webhook arrives.",
    "steps": [
      "Create a secret key in the Tempest Touch developer dashboard.",
      "Create a charge using POST /charges with an Idempotency-Key header.",
      "Send the customer to charge.hosted_url or use the returned pay address.",
      "After a terminal payment status, hosted checkout returns to success_url or cancel_url with tempest_status, tempest_message and tempest_charge_id query parameters so your site can display a notification.",
      "Listen for signed webhook events and verify them using the raw body.",
      "Mark orders as fulfilled only after a verified charge.paid event."
    ]
  },
  "endpoints": {
    "ping": {
      "method": "GET",
      "path": "/ping",
      "description": "Checks whether the provided secret key is valid and returns livemode info."
    },
    "createCharge": {
      "method": "POST",
      "path": "/charges",
      "description": "Create a hosted checkout charge and return hosted_url plus the payment address. Terminal checkout returns append payment outcome query parameters to the configured success or cancel URL.",
      "requiredHeaders": [
        "Authorization",
        "Content-Type",
        "Idempotency-Key"
      ],
      "requiredBody": [
        "amount",
        "currency",
        "title",
        "reference"
      ],
      "optionalBody": [
        "description",
        "customer_email",
        "success_url",
        "cancel_url",
        "expires_in",
        "accepted",
        "metadata"
      ],
      "idempotency": "The Idempotency-Key header prevents duplicate charges when the same request is retried."
    },
    "getCharge": {
      "method": "GET",
      "path": "/charges/{id}",
      "description": "Fetch the current status and details for a charge."
    },
    "listCharges": {
      "method": "GET",
      "path": "/charges",
      "description": "List charges with filters such as limit, status, reference and starting_after."
    },
    "cancelCharge": {
      "method": "POST",
      "path": "/charges/{id}/cancel",
      "description": "Cancel a charge before it is paid."
    },
    "simulatePayment": {
      "method": "POST",
      "path": "/test/charges/{id}/simulate",
      "description": "Only available with test keys. Simulates a payment for a test charge."
    }
  },
  "webhooks": {
    "delivery": "Signed JSON POST requests with a Tempest-Signature header.",
    "verification": {
      "rawBodyRequired": true,
      "signatureHeader": "Tempest-Signature",
      "headerFormat": "t=<unix_timestamp>,v1=<hex_signature>",
      "algorithm": "HMAC-SHA256 over timestamp + \".\" + raw body bytes",
      "toleranceSeconds": 300
    },
    "eventTypes": [
      "charge.created",
      "charge.processing",
      "charge.partial_payment",
      "charge.paid",
      "charge.underpaid",
      "charge.expired",
      "charge.cancelled",
      "charge.late_payment"
    ],
    "reliability": "Use event.id for idempotency and retry any 2xx heartbeat failure. The same event can be delivered more than once."
  },
  "statusFlow": {
    "pending": "Charge is open and waiting for payment.",
    "processing": "Money was seen but is awaiting network confirmations.",
    "partial": "A partial payment is in progress.",
    "paid": "The charge is fully paid.",
    "overpaid": "The charge was paid above the required total.",
    "underpaid": "The charge expired before the amount was completed.",
    "expired": "The charge expired without a valid payment.",
    "cancelled": "The charge was cancelled by the merchant or customer."
  },
  "examples": {
    "node": "const BASE = 'https://tempesttouchwallet.com/api/v1';\n\nexport async function createCharge({ amount, currency = 'USD', title, reference }) {\n  const res = await fetch(BASE + '/charges', {\n    method: 'POST',\n    headers: {\n      Authorization: 'Bearer ' + process.env.TEMPEST_SECRET_KEY,\n      'Content-Type': 'application/json',\n      'Idempotency-Key': reference,\n    },\n    body: JSON.stringify({ amount, currency, title, reference }),\n  });\n  if (!res.ok) throw new Error('Charge failed: ' + res.status);\n  return res.json();\n}",
    "python": "import os, requests\n\nBASE = 'https://tempesttouchwallet.com/api/v1'\n\ndef create_charge(amount, title, reference, currency='USD'):\n    r = requests.post(\n        BASE + '/charges',\n        headers={\n            'Authorization': 'Bearer ' + os.environ['TEMPEST_SECRET_KEY'],\n            'Content-Type': 'application/json',\n            'Idempotency-Key': reference,\n        },\n        json={'amount': amount, 'currency': currency, 'title': title, 'reference': reference},\n        timeout=30,\n    )\n    r.raise_for_status()\n    return r.json()\n",
    "expressWebhook": "import crypto from 'node:crypto';\nimport express from 'express';\n\napp.post('/webhooks/tempest', express.raw({ type: '*/*' }), (req, res) => {\n  const signature = req.get('Tempest-Signature') || '';\n  const parts = signature.split(',');\n  const data = Object.fromEntries(parts.map((entry) => entry.split('=', 2)));\n  const timestamp = Number(data.t);\n  const raw = Buffer.from(req.body);\n  const expected = crypto.createHmac('sha256', process.env.TEMPEST_WEBHOOK_SECRET).update(String(timestamp) + '.').update(raw).digest('hex');\n  const valid = Number.isFinite(timestamp) && Math.abs(Date.now() / 1000 - timestamp) <= 300 && parts.some((part) => part.startsWith('v1=') && crypto.timingSafeEqual(Buffer.from(part.slice(3), 'hex'), Buffer.from(expected, 'hex')));\n  if (!valid) return res.sendStatus(400);\n  const event = JSON.parse(raw.toString('utf8'));\n  if (event.type === 'charge.paid') {\n    // mark order fulfilled with event.data.object.reference\n  }\n  return res.sendStatus(200);\n});\n"
  },
  "notes": [
    "Amounts are strings, for example \"25.00\".",
    "Coins are selected by network/symbol pairs such as base:USDC or ethereum:USDT.",
    "Redirect URLs must be public HTTPS URLs.",
    "The hosted checkout page is the recommended user experience for merchants and marketplaces.",
    "Checkout return parameters are customer-facing only; verify the signed webhook or retrieve the charge server-side before fulfilling orders."
  ]
}