# SatsCollect > SatsCollect is a hosted, self-custodial Bitcoin checkout. Vendors provide their own on-chain address pool. SatsCollect assigns an unused address, locks fiat-to-BTC pricing, detects confirmed payment, and returns a signed result. Canonical integration documentation: https://www.satscollect.com/docs ## Plans - Basic: fixed-price products, BTC address pool, hosted checkout, signed confirmation redirect. - Pro: signed dynamic pricing and payment.confirmed webhooks, in addition to Basic features. ## Setup 1. Create a vendor in the SatsCollect dashboard. 2. Copy the 64-character vendor secret and store it only on your backend. 3. Add unused Bitcoin mainnet addresses controlled by the vendor. Each checkout reserves one address. 4. Create a product with a price in btc, usd, eur, or ron and an optional thank_you_url. 5. Use the product UUID in the checkout URL. ## Fixed-price checkout (Basic) GET https://satscollect.com/pay/{productUuid} No signature is needed. The checkout uses the product's configured amount and currency. ## Dynamic-price checkout (Pro) GET https://satscollect.com/pay/{productUuid}?payload={encodedPayload}&signature={signature} Payload JSON fields: - amount: positive number - currency: btc, usd, eur, or ron - return_url: optional HTTPS URL Encode the JSON with base64 or unpadded base64url. Compute: signature = lowercase_hex(HMAC_SHA256(encodedPayload, vendorSecret)) Sign the exact encoded payload string placed in the URL. Generate the URL server-side; never expose vendorSecret to a browser or mobile client. PHP example: $payload = ['amount' => 49.90, 'currency' => 'usd', 'return_url' => 'https://shop.example.com/paid']; $encoded = rtrim(strtr(base64_encode(json_encode($payload)), '+/', '-_'), '='); $signature = hash_hmac('sha256', $encoded, $_ENV['SATSCOLLECT_SECRET']); ## Confirmation redirect (Basic and Pro) After confirmed payment, SatsCollect redirects to payload.return_url when present, otherwise the product thank_you_url. It appends: - payment_id: numeric SatsCollect payment ID - token: lowercase hex HMAC-SHA256 of the decimal payment_id, keyed by vendorSecret Verification: $expected = hash_hmac('sha256', $_GET['payment_id'], $_ENV['SATSCOLLECT_SECRET']); if (! hash_equals($expected, $_GET['token'] ?? '')) { http_response_code(401); exit; } The SatsCollect redirect is one-time. Make fulfillment idempotent and ensure the payment maps to the expected local order. ## payment.confirmed webhook (Pro) Configure webhook_url on the vendor. SatsCollect sends an application/json POST after payment confirmation. Headers: - X-SatsCollect-Event: payment.confirmed - X-SatsCollect-Signature: lowercase hex HMAC-SHA256 of the raw JSON request body, keyed by vendorSecret Verify the raw bytes before JSON parsing: $raw = file_get_contents('php://input'); $expected = hash_hmac('sha256', $raw, $_ENV['SATSCOLLECT_SECRET']); if (! hash_equals($expected, $_SERVER['HTTP_X_SATSCOLLECT_SIGNATURE'] ?? '')) { http_response_code(401); exit; } Webhook JSON shape: { "event": "payment.confirmed", "payment_request": { "id": 1842, "uuid": "payment-request-uuid", "vendor_id": 12, "product_id": 48, "product_uuid": "product-uuid", "address": "bc1q...", "expected_amount": "0.00072410", "received_amount": "0.00072410", "confirmed": true, "paid_at": "ISO-8601 timestamp", "currency": "usd", "fiat_amount": "49.90", "payload": "original encoded payload or null", "return_url": "decoded return URL or null" } } Current automatic webhook delivery is one attempt with a 10-second timeout. Delivery attempts are recorded, and confirmed payments can be manually resent from the vendor transaction list. Return 2xx quickly and process idempotently. ## Lifecycle and operational details - Fiat checkout prices are converted through the configured BTC price provider and locked for 15 minutes. - BTC product prices use the configured BTC amount directly. - Payment status is checked about every two minutes against confirmed on-chain received totals. - Payment confirms when received BTC is greater than or equal to expected_amount. - Unpaid requests are deleted and their addresses released 48 hours after price-lock expiry. - A checkout returns HTTP 500 when the vendor has no unused address. - The browser polls GET /pay/{paymentRequestUuid}/status, which returns {"confirmed": boolean}. ## Security requirements - Vendor secrets belong only on trusted backend systems. - Compare signatures with a constant-time function. - Verify the webhook's original raw body, not re-serialized JSON. - Use HTTPS webhook and return URLs. - Fulfill exactly once, keyed by payment_request.id or payment_request.uuid. - Monitor and replenish unused Bitcoin addresses.