Initial backup of root workspace files
Glue files not covered by subproject repos: top-level docs, logo, keysat-design-system, and crosscheck tests. Subproject folders are gitignored (each has its own Gitea remote).
This commit is contained in:
@@ -0,0 +1,501 @@
|
||||
# Zaprite integration spec
|
||||
|
||||
Multi-provider payments for Keysat — adding Zaprite alongside BTCPay so
|
||||
operators can optionally accept card and broader-rail payments. This spec
|
||||
captures architecture, the Zaprite API shape (insofar as it can be
|
||||
verified from public sources), trade-offs, and a phased build plan.
|
||||
|
||||
## Why this matters
|
||||
|
||||
Keysat today only sells to people who have or will set up a Bitcoin
|
||||
wallet. That's a deliberate sovereignty choice but it caps the
|
||||
addressable market. Zaprite is a payment platform that sits one layer
|
||||
above payment processors and accepts:
|
||||
|
||||
- Bitcoin on-chain
|
||||
- Lightning (via the operator's own LN node, Strike, Voltage, etc.)
|
||||
- Card payments (Visa/Mastercard) via Stripe or Square
|
||||
- Liquid Bitcoin
|
||||
|
||||
For a Keysat operator, plugging Zaprite in means anyone with a credit
|
||||
card can buy their software. The operator still chooses what they get
|
||||
paid in (BTC settled to their wallet, USD settled to their bank account,
|
||||
etc.).
|
||||
|
||||
Sovereignty cost: Zaprite is a SaaS. Card payments require KYC with a
|
||||
processor. Customer PII flows through Zaprite. We mark this clearly in
|
||||
the operator-facing UI; sovereignty stays the default, fiat is opt-in.
|
||||
|
||||
## Architecture: PaymentProvider abstraction
|
||||
|
||||
Today the daemon hard-codes BTCPay assumptions throughout invoice
|
||||
creation, webhook handling, and reconciliation. The right shape is a
|
||||
trait that BTCPay and Zaprite both implement:
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait PaymentProvider: Send + Sync {
|
||||
/// What kind of provider this is — for logs, audit, and admin UI.
|
||||
fn kind(&self) -> ProviderKind;
|
||||
|
||||
/// Create a hosted-checkout session. Returns the public checkout URL
|
||||
/// that the buyer is redirected to, plus a provider-side invoice id
|
||||
/// the daemon stores in the `invoices` table.
|
||||
async fn create_invoice(
|
||||
&self,
|
||||
params: CreateInvoiceParams<'_>,
|
||||
) -> Result<CreatedInvoiceHandle>;
|
||||
|
||||
/// Fetch invoice state on demand (for reconciliation on startup).
|
||||
async fn get_invoice_status(&self, invoice_id: &str) -> Result<InvoiceStatus>;
|
||||
|
||||
/// Validate a webhook delivery. Returns the parsed event or an
|
||||
/// auth/parse error.
|
||||
fn validate_webhook(
|
||||
&self,
|
||||
headers: &HeaderMap,
|
||||
body: &[u8],
|
||||
) -> Result<WebhookEvent>;
|
||||
|
||||
/// Pay a Lightning invoice — used by the tip-recipient flow when the
|
||||
/// provider has an outgoing-LN capability. Optional; some providers
|
||||
/// won't support it (return ProviderCapabilityNotSupported).
|
||||
async fn pay_lightning_invoice(&self, bolt11: &str) -> Result<PaymentReceipt>;
|
||||
}
|
||||
|
||||
pub enum ProviderKind { Btcpay, Zaprite }
|
||||
|
||||
pub struct CreateInvoiceParams<'a> {
|
||||
pub amount: Money, // {value, currency} — sats or USD or whatever
|
||||
pub redirect_url: &'a str,
|
||||
pub metadata: &'a serde_json::Value,
|
||||
pub external_order_id: &'a str,// Keysat's invoice.id — passed back in webhooks
|
||||
pub idempotency_key: &'a str,
|
||||
pub buyer_email: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub enum WebhookEvent {
|
||||
InvoiceSettled { external_order_id: String, paid_amount: Money, .. },
|
||||
InvoiceExpired { external_order_id: String },
|
||||
InvoiceRefunded { external_order_id: String, refunded_amount: Money },
|
||||
Other { kind: String, body: serde_json::Value },
|
||||
}
|
||||
```
|
||||
|
||||
The rest of Keysat (license issuance, audit logging, admin UI) becomes
|
||||
provider-agnostic. The chosen provider is held in `AppState`:
|
||||
|
||||
```rust
|
||||
pub struct AppState {
|
||||
pub db: SqlitePool,
|
||||
pub keypair: Arc<ServerKeypair>,
|
||||
pub config: Arc<Config>,
|
||||
pub self_tier: Arc<RwLock<license_self::Tier>>,
|
||||
pub payment: Arc<RwLock<Option<Box<dyn PaymentProvider>>>>, // was: btcpay
|
||||
}
|
||||
```
|
||||
|
||||
Per-operator, not per-product. v1: an operator picks ONE provider during
|
||||
setup. All products go through it. Switching is an explicit Disconnect →
|
||||
Connect operation. Per-product provider selection is deferred.
|
||||
|
||||
## What I learned about Zaprite's API (and what's still unknown)
|
||||
|
||||
Zaprite egress is blocked from this Cowork sandbox so I couldn't read
|
||||
api.zaprite.com directly. The information below is reconstructed from
|
||||
their open-source WooCommerce plugin
|
||||
([github.com/ZapriteApp/zaprite-for-woocommerce](https://github.com/ZapriteApp/zaprite-for-woocommerce))
|
||||
and their official integration patterns. **Confirm everything before
|
||||
writing production code; I'm flagging which pieces need verification.**
|
||||
|
||||
### What I'm confident about
|
||||
|
||||
The plugin's `zaprite_api.php` shows the create-order shape clearly:
|
||||
|
||||
```php
|
||||
$data = array(
|
||||
'apiKey' => $this->api_key,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency, // 'USD', 'BTC', 'SAT', 'EUR'
|
||||
'orderUpdateCallback' => $callback_url, // webhook URL
|
||||
'redirectUrl' => $complete_url,
|
||||
'externalOrderId' => $order_id,
|
||||
'externalUniqId' => $idempotency_key,
|
||||
);
|
||||
```
|
||||
|
||||
Notable: **the API key is passed in the JSON body, not in a header.**
|
||||
This is unusual and worth confirming — the public `/v1/order` endpoint
|
||||
(used by the api-demo repo, separate from the WooCommerce-specific
|
||||
endpoint) likely uses a header instead. Zaprite has been beta-API for
|
||||
a while; the conventions may have settled differently for `/v1/*`.
|
||||
|
||||
Endpoints, confirmed-by-source (WooCommerce plugin):
|
||||
- `POST {base}/api/public/woo/create-order` — plugin-specific
|
||||
- `POST {base}/api/public/woo/check-order` — plugin-specific
|
||||
|
||||
Endpoints, confirmed-by-mention (api-demo repo):
|
||||
- `POST {base}/v1/order` — general public API
|
||||
- `GET {base}/v1/order/:id` — general public API
|
||||
|
||||
Webhook delivery: Zaprite POSTs to the URL the operator passed in
|
||||
`orderUpdateCallback`. The body includes order state. The plugin code
|
||||
I saw does NOT verify a signature — it relies on the secret-ness of
|
||||
`externalUniqId` plus matching it against the locally-tracked order.
|
||||
That's not great security; the public `/v1/*` API may have a proper
|
||||
HMAC scheme.
|
||||
|
||||
Currencies: at least `USD`, `BTC`, `SAT`, `EUR`. The plugin lets the
|
||||
merchant configure store currency in WooCommerce and passes it
|
||||
through.
|
||||
|
||||
Settlement model: from blog content I saw, the operator chooses how
|
||||
each payment rail settles. BTC payments settle to a wallet they
|
||||
configure (Strike, Unchained, BTCPay, lightning node, etc.). Card
|
||||
payments settle to their connected Stripe or Square account in fiat.
|
||||
Zaprite doesn't custody funds.
|
||||
|
||||
Recurring billing: confirmed in marketing copy ("recurring invoices").
|
||||
API specifics not visible; almost certainly distinct endpoints from
|
||||
one-shot orders.
|
||||
|
||||
Pricing: $300/year subscription includes $300 of transaction fees
|
||||
(monthly subscriptions $25/mo include $25/mo of transaction fees). Above
|
||||
that, Stripe's standard 2.9% + 30¢ on cards plus Zaprite's own fee
|
||||
(percentage unclear). BTC/Lightning payments through the operator's own
|
||||
node are free of Zaprite-side fees on transactions; the subscription
|
||||
pays for the platform itself.
|
||||
|
||||
### Open questions — resolved (May 2026)
|
||||
|
||||
Resolved by reading Zaprite's actual OpenAPI spec at
|
||||
`https://api.zaprite.com/openapi.json` and the LLM-friendly summary
|
||||
at `https://api.zaprite.com/llms.txt`. Six of the original seven
|
||||
questions are now answered; one remains.
|
||||
|
||||
1. **Auth on `/v1/orders`** — Bearer token. ✅
|
||||
`Authorization: Bearer <api_key>`. One key per Zaprite
|
||||
organization. Keys created/rotated/revoked at
|
||||
`app.zaprite.com/org/default/settings/api`.
|
||||
|
||||
2. **Webhook signature scheme** — ❌ STILL UNKNOWN.
|
||||
Zaprite's public docs confirm webhooks exist and that endpoints
|
||||
should return 200 (else Zaprite retries), but the signature
|
||||
scheme isn't published. Three plausible options based on the
|
||||
operator-feedback pattern: (a) HMAC-SHA256 of the raw body in
|
||||
an `X-Zaprite-Signature` header — most common shape, what
|
||||
Stripe/BTCPay/GitHub use; (b) a JWT signed with a per-webhook
|
||||
secret; (c) no signature, in which case operators are expected
|
||||
to verify via the `externalUniqId` + a private-URL convention.
|
||||
Resolution path: when the operator creates a webhook in their
|
||||
Zaprite dashboard, the form likely shows a signing secret; the
|
||||
tooltip / docs link from THAT page should describe the exact
|
||||
scheme. We implement the impl as a small trait-internal
|
||||
function that's swappable once the truth is known. Initial
|
||||
implementation: HMAC-SHA256 in `X-Zaprite-Signature`,
|
||||
constant-time compare, fall through to a "log but accept"
|
||||
mode in dev for the first integration test.
|
||||
|
||||
3. **Currency code for sats** — `BTC`. ✅
|
||||
Zaprite's currency enum has `BTC` (also `LBTC` for Liquid).
|
||||
Amounts are in the smallest indivisible unit per currency, so
|
||||
`currency: "BTC"` + `amount: 50000` means 50,000 sats. There's
|
||||
no separate `SAT` code; `BTC` covers both display
|
||||
denominations cleanly.
|
||||
|
||||
4. **Recurring billing** — PARTIAL. ✅ (with caveat)
|
||||
Zaprite has NO native subscription endpoints (no auto-renewing
|
||||
schedules). What it offers:
|
||||
- `allowSavePaymentProfile: true` on order creation — buyer
|
||||
can save their card during checkout
|
||||
- `Contact.paymentProfiles[]` — saved profiles per contact
|
||||
- `POST /v1/orders/charge` with `paymentProfileId` — programmatic
|
||||
charge against a saved profile
|
||||
So Keysat's recurring-subscription scheduler (per
|
||||
`RECURRING_SUBSCRIPTIONS_DESIGN.md`) drives the cycle on its
|
||||
side: at each renewal, the daemon's renewal worker creates a
|
||||
new order against the buyer's saved Zaprite payment profile.
|
||||
This is actually CLEANER than relying on Zaprite-managed
|
||||
subscriptions because Keysat keeps the source of truth on
|
||||
when to bill. Trade-off: the buyer's save-card flow happens
|
||||
on the FIRST purchase; subsequent cycles are charge-without-
|
||||
redirect.
|
||||
|
||||
5. **Sandbox / test mode** — ✅ Two options:
|
||||
- **Sandbox organization** — operators can spin up a separate
|
||||
org marked as sandbox. Same API endpoints; org context
|
||||
determines real-money vs test. Recommended for staging.
|
||||
- **Test Payment plugin** — within a normal org, activate
|
||||
"Test Payment" at
|
||||
`app.zaprite.com/org/default/connections/testPayment` to
|
||||
trigger PENDING/CONFIRMED states without real funds. Useful
|
||||
for one-off integration tests.
|
||||
|
||||
6. **Refund endpoint** — ❌ Not in the public API.
|
||||
Operators handle refunds via the Zaprite dashboard. A refund
|
||||
produces some webhook event (presumably `order.refunded` or
|
||||
similar — see resolution path on Q2). For Keysat's purposes
|
||||
this is fine: we listen for the refund event and revoke the
|
||||
license; we don't need to initiate refunds programmatically
|
||||
from Keysat.
|
||||
|
||||
7. **Idempotency** — `externalUniqId` is reconciliation, not
|
||||
deduplication. ✅
|
||||
Sending the same `externalUniqId` twice DOES create two
|
||||
orders. Keysat's purchase flow already mints a UUID on
|
||||
invoice-row creation BEFORE calling create_invoice, and we
|
||||
guard against double-call client-side via the existing
|
||||
reservation pattern. No change needed.
|
||||
|
||||
### Endpoints we'll use
|
||||
|
||||
```
|
||||
POST /v1/orders — create order (one-shot or recurring cycle)
|
||||
GET /v1/orders/{id} — get order status (id can be Zaprite id OR externalUniqId)
|
||||
GET /v1/orders — list (paginated, 100/page)
|
||||
POST /v1/orders/charge — charge a saved paymentProfile (recurring cycles)
|
||||
POST /v1/webhooks — operator manages webhook endpoints
|
||||
```
|
||||
|
||||
Errors come back as `{ code, message, issues: [{ message }] }`
|
||||
with HTTP status codes `400`/`401`/`403`/`404`/`500`. Our
|
||||
ZapriteProvider impl wraps these into `AppError::Upstream` for
|
||||
consistency with the BtcpayProvider error surface.
|
||||
|
||||
### What we still need from Grant
|
||||
|
||||
Just one thing, when convenient:
|
||||
|
||||
1. **Create an API key** at the URL in your screenshot. Label it
|
||||
`keysat-test`. **Keep the secret on your machine** — paste it
|
||||
into Keysat via a future "Connect Zaprite" admin action when
|
||||
the impl is ready; don't share it in chat.
|
||||
2. **Create a sandbox webhook** (or any webhook) and screenshot
|
||||
the form. Specifically: when you click "Add Webhook" in the
|
||||
API tab, the form likely shows a "Signing Secret" field and
|
||||
either a tooltip or a docs link explaining how Zaprite signs
|
||||
payloads. That's the last bit we need.
|
||||
|
||||
The implementation can proceed today on points 1, 3-7 (which is
|
||||
most of it). Point 2 (webhook signing) gets stubbed with a
|
||||
best-guess HMAC-SHA256 + `X-Zaprite-Signature` impl that we
|
||||
correct the moment your screenshot reveals the actual scheme.
|
||||
|
||||
## Data model changes
|
||||
|
||||
### Multi-currency on products
|
||||
|
||||
```sql
|
||||
ALTER TABLE products ADD COLUMN price_currency TEXT NOT NULL DEFAULT 'SAT';
|
||||
ALTER TABLE products ADD COLUMN price_value INTEGER NOT NULL DEFAULT 0;
|
||||
-- old price_sats becomes a derived/legacy view
|
||||
```
|
||||
|
||||
Currencies live as ISO-style strings: `SAT`, `USD`, `EUR`, `BTC`. UI
|
||||
displays "$30 USD" or "50,000 sats" with a toggle. We do NOT do
|
||||
exchange-rate conversion server-side — operators denominate in one
|
||||
currency, buyers pay in whatever the chosen provider supports for that
|
||||
currency. Future: optional dual-display ("$30 / ~52,300 sats at current
|
||||
rate") but defer.
|
||||
|
||||
### Provider tag on invoices
|
||||
|
||||
```sql
|
||||
ALTER TABLE invoices ADD COLUMN provider TEXT NOT NULL DEFAULT 'btcpay';
|
||||
ALTER TABLE invoices ADD COLUMN provider_invoice_id TEXT; -- already exists as btcpay_invoice_id; rename
|
||||
```
|
||||
|
||||
Each invoice records which provider handled it. Critical for refund
|
||||
flows, chargeback handling, and historical reporting once both providers
|
||||
are in use.
|
||||
|
||||
### Refund / chargeback support
|
||||
|
||||
Card payments mean chargebacks. We need a `license_revoked_due_to_refund`
|
||||
flow:
|
||||
|
||||
```sql
|
||||
ALTER TABLE licenses ADD COLUMN revoked_reason TEXT;
|
||||
ALTER TABLE licenses ADD COLUMN revoked_at TEXT;
|
||||
```
|
||||
|
||||
When Zaprite delivers an `InvoiceRefunded` webhook, the daemon revokes
|
||||
the corresponding license, records the reason, fires a webhook
|
||||
subscriber notification (`license.revoked` event with reason
|
||||
`payment_refunded`), and the operator's records update.
|
||||
|
||||
## Phased build plan
|
||||
|
||||
### Phase 1 — PaymentProvider abstraction (v0.2)
|
||||
|
||||
Refactor the existing BTCPay code into the first impl of the trait. No
|
||||
user-visible change. Validates the abstraction.
|
||||
|
||||
| Task | Effort |
|
||||
|---|---|
|
||||
| Define `PaymentProvider` trait, `Money`, `WebhookEvent`, `CreateInvoiceParams` types | S |
|
||||
| Move existing `BtcpayClient` behind a `BtcpayProvider: PaymentProvider` impl | M |
|
||||
| Replace `state.btcpay` with `state.payment: Arc<RwLock<Option<Box<dyn PaymentProvider>>>>` | S |
|
||||
| Update webhook handler to dispatch on `provider` field of the matching invoice row | S |
|
||||
| Update reconcile loop to call provider-agnostic `get_invoice_status` | S |
|
||||
| Audit: every existing reference to "btcpay" in user-facing copy stays unchanged in v0.2 (provider chooser ships in v0.3) | XS |
|
||||
|
||||
**Net: ~2 days. Lands as v0.2.0:0.**
|
||||
|
||||
### Phase 2 — Multi-currency data model (v0.2 or v0.3)
|
||||
|
||||
| Task | Effort |
|
||||
|---|---|
|
||||
| Migration 0007 adds `price_currency`, `price_value` to products; `provider`, `provider_invoice_id` to invoices; `revoked_reason`, `revoked_at` to licenses | S |
|
||||
| Update Product / Invoice / License models | S |
|
||||
| Admin UI: currency selector on create-product form; rendering shows currency-correct units everywhere | M |
|
||||
| Marketing-page integration: purchase URL displays correct currency | S |
|
||||
|
||||
**Net: ~1.5 days. Can ride with Phase 1 in v0.2 if there's headroom; otherwise lands in v0.3.**
|
||||
|
||||
### Phase 3 — ZapriteProvider implementation (v0.3)
|
||||
|
||||
| Task | Effort |
|
||||
|---|---|
|
||||
| Verify the open Zaprite API questions (auth scheme, webhook signature, currency codes) — Grant + me reading the docs together | S |
|
||||
| Build `ZapriteProvider` impl: create_invoice, get_invoice_status, validate_webhook | M-L (depends on signature scheme; if HMAC, ~1 day; if no signature + state-matching, ~1.5 days) |
|
||||
| Map `WebhookEvent::InvoiceRefunded` → license revoke flow | S |
|
||||
| Test with sandbox keys + a real low-value card transaction | S |
|
||||
|
||||
**Net: 3-5 days. Lands in v0.3 alongside recurring billing.**
|
||||
|
||||
### Phase 4 — StartOS actions for Zaprite (v0.3)
|
||||
|
||||
| Task | Effort |
|
||||
|---|---|
|
||||
| `Connect Zaprite` action — operator pastes API key, daemon validates by hitting Zaprite's `/v1/order` test endpoint, persists, sets as active provider | S |
|
||||
| `Check Zaprite connection` action | XS |
|
||||
| `Disconnect Zaprite` action — clears stored API key, drops back to "no provider" state | XS |
|
||||
| `Switch provider` action — when operator has both BTCPay and Zaprite connected, lets them flip the active provider | S |
|
||||
|
||||
**Net: ~1 day. Lands in v0.3 with Phase 3.**
|
||||
|
||||
### Phase 5 — Admin UI updates (v0.3)
|
||||
|
||||
| Task | Effort |
|
||||
|---|---|
|
||||
| Sidebar BTCPay status indicator becomes "Payment provider" indicator with provider-kind badge | XS |
|
||||
| Create-product form: currency selector | S |
|
||||
| Invoices list: show provider column | XS |
|
||||
| New "Refunds" section in admin UI: lists `license.revoked` audit entries with `reason: payment_refunded` | S |
|
||||
| Provider chooser in onboarding wizard ("Connect a payment provider — BTCPay or Zaprite or skip for now") | M |
|
||||
|
||||
**Net: ~1.5 days. Lands in v0.3.**
|
||||
|
||||
### Phase 6 — Recurring billing via Zaprite (v0.3)
|
||||
|
||||
This is the headline v0.3 feature anyway; sequencing Zaprite work here
|
||||
is intentional.
|
||||
|
||||
| Task | Effort |
|
||||
|---|---|
|
||||
| `subscriptions` table migration | S |
|
||||
| Subscription lifecycle: created → trialing → active → past_due → canceled | M |
|
||||
| Provider-side recurring-invoice setup via Zaprite's recurring API (assuming it exists; verify in Phase 3) | M-L |
|
||||
| Auto-renewal webhook handling — settled webhook on a recurring invoice extends license expiry | S |
|
||||
| Customer-facing "manage subscription" link in receipt emails | S |
|
||||
| Admin UI subscription views | M |
|
||||
|
||||
**Net: 4-6 days. Lands in v0.3.**
|
||||
|
||||
### Phase 7 — Documentation (v0.3)
|
||||
|
||||
| Task | Effort |
|
||||
|---|---|
|
||||
| Operator runbook: signing up for Zaprite, getting API access, setting up payment rails (Strike / Unchained / Stripe / Square) | S |
|
||||
| KYC implications callout | XS |
|
||||
| Refund + chargeback flow doc | XS |
|
||||
| Fee breakdown comparison table (BTCPay-Lightning vs Zaprite-card vs Zaprite-LN-via-Strike etc.) | S |
|
||||
| Update integration guide to mention multi-currency | XS |
|
||||
| Update marketing page to mention "or accept card payments via Zaprite" once Phase 3 ships | S |
|
||||
|
||||
**Net: ~1.5 days.**
|
||||
|
||||
### Phase 8 — End-to-end testing (v0.3)
|
||||
|
||||
| Task | Effort |
|
||||
|---|---|
|
||||
| Real sandbox or low-value tests for: Zaprite create-order → buyer pays card → webhook arrives → license issued | S |
|
||||
| Test the recurring-renewal path with at least two cycles | S |
|
||||
| Test refund: refund in Zaprite → webhook arrives → license revoked | S |
|
||||
| Test provider switching: BTCPay active → switch to Zaprite → existing pending BTCPay invoices still reconcile correctly | S |
|
||||
| Test currency mixing: products in USD, products in SAT, both work in their respective providers | S |
|
||||
|
||||
**Net: 1-2 days.**
|
||||
|
||||
## Total effort
|
||||
|
||||
7-13 days of focused build work, plus testing. Realistic target: 10
|
||||
days. That's a v0.2.0 (Phase 1, possibly Phase 2) → v0.3.0 (Phases 3-8)
|
||||
arc, not a single release.
|
||||
|
||||
## Trade-offs and gotchas to be loud about in operator docs
|
||||
|
||||
- **Stripe/Zaprite fees stack on card payments.** A $30 card payment:
|
||||
$0.87 to Stripe + ~$0.20-0.50 to Zaprite + 30¢ processor fee ≈ $1.40,
|
||||
about 4.6%. BTCPay-Lightning is near-zero. Operators should know this
|
||||
when pricing.
|
||||
- **Card payments mean KYC with Stripe.** The operator becomes a
|
||||
merchant of record. Identity verified, bank account on file, business
|
||||
address required. This is genuinely incompatible with the
|
||||
privacy-preserving operator model some Keysat users want; we should be
|
||||
loud about it in the UI and in docs.
|
||||
- **Customer PII flows through Zaprite and Stripe.** With BTCPay-only,
|
||||
buyer email + npub + invoice id all live on the operator's Start9.
|
||||
Zaprite path adds buyer name, card metadata, billing address, IP, and
|
||||
potentially more in Zaprite's and Stripe's databases. Privacy policy
|
||||
needs to reflect this.
|
||||
- **Refund flow is provider-specific.** BTC: operator manually sends
|
||||
sats back. Cards: Stripe-initiated refund via Zaprite's UI (or API),
|
||||
webhook arrives, license auto-revokes. Two different UX paths, both
|
||||
documented.
|
||||
- **Subscription cancellation.** With recurring billing, buyers can
|
||||
cancel via their card issuer (chargeback) or via a self-service portal
|
||||
we'd build. Both paths need to fire `subscription.canceled` and update
|
||||
license expiry semantics.
|
||||
|
||||
## What this enables longer term
|
||||
|
||||
The PaymentProvider abstraction is defensive even if Zaprite never ships
|
||||
— it's the right shape for adding any future provider:
|
||||
|
||||
- **Strike API directly.** Skip Zaprite entirely; integrate Strike's API
|
||||
for sat-denominated payments without going through their checkout.
|
||||
- **Coinbase Commerce.** Easy add-on for operators who already have a
|
||||
Coinbase Commerce account.
|
||||
- **OpenNode.** Another Bitcoin-native processor.
|
||||
- **PayPal / direct Square.** For operators willing to skip Zaprite.
|
||||
|
||||
Each is a new `PaymentProvider` impl. ~3-5 days each once the
|
||||
abstraction is in.
|
||||
|
||||
## Sources
|
||||
|
||||
- [Zaprite developers page](https://zaprite.com/developers) (documented
|
||||
but inaccessible from this sandbox; Grant should read directly)
|
||||
- [Zaprite API & webhooks blog post](https://blog.zaprite.com/opening-zaprites-api-webhooks/)
|
||||
- [Zaprite WooCommerce plugin source — `zaprite_api.php`](https://github.com/ZapriteApp/zaprite-for-woocommerce/blob/main/zaprite-payment-gateway/includes/zaprite_api.php)
|
||||
- [Zaprite api-demo repo](https://github.com/zapriteapp/zaprite-api-demo)
|
||||
- [Zaprite pricing page](https://zaprite.com/pricing)
|
||||
- [Zaprite API reference](https://api.zaprite.com/) (egress-blocked from
|
||||
this sandbox — Grant has direct access)
|
||||
|
||||
## My next step recommendations
|
||||
|
||||
1. **Grant signs up for a free Zaprite account, requests API access at
|
||||
Settings → API, and pastes back what api.zaprite.com/v1 actually
|
||||
shows for create-order and webhooks.** That answers the seven open
|
||||
questions above and lets me write production-grade code.
|
||||
2. **Phase 1 (PaymentProvider abstraction) can start now without any
|
||||
Zaprite specifics** — it's a refactor of existing BTCPay code. I can
|
||||
do this autonomously and have it ready for v0.2.0:0 alongside the
|
||||
web UI auth hardening already queued.
|
||||
3. **Phases 2-8 wait on the Zaprite API confirmation.** No point coding
|
||||
against a guessed-at API.
|
||||
|
||||
Want me to start Phase 1 now while you go grab the Zaprite docs?
|
||||
Reference in New Issue
Block a user