sassydigitizing / whop-integration
Whop payment integration: server-side checkout/plan/product creation, Standard-Webhooks-spec signature verification, and payment re-confirmation. Framework-agnostic — the library takes plain constructor arguments, no env/config coupling.
Package info
github.com/shamama-007/sassy-whop-payment-gateway
pkg:composer/sassydigitizing/whop-integration
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^10.5
- vlucas/phpdotenv: ^5.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-07 04:53:16 UTC
README
A small, framework-agnostic PHP library for Whop payments: server-side checkout/plan/product creation, Standard-Webhooks-spec signature verification, and payment re-confirmation. Every class takes plain constructor arguments — no env-loading, no framework coupling, no database requirement in the library itself.
Ships with a reference implementation (bootstrap.php, webhooks/whop.php,
public/*.php) showing one way to wire it end-to-end, meant to be copied and
adapted into your own app rather than depended on directly.
Installation
This package isn't published on Packagist. Install it either as a local path dependency (fastest, no hosting needed) or from your own git remote.
Path repository (the package lives on the same machine / same monorepo):
{
"repositories": [
{ "type": "path", "url": "../whop-integration" }
],
"require": {
"sassydigitizing/whop-integration": "*"
}
}
composer update sassydigitizing/whop-integration
VCS repository (once you've pushed this to a git host — GitHub, GitLab, a private Bitbucket, etc.):
{
"repositories": [
{ "type": "vcs", "url": "git@github.com:your-org/whop-integration.git" }
],
"require": {
"sassydigitizing/whop-integration": "^1.0"
}
}
composer require sassydigitizing/whop-integration
Either way, only src/ (the actual library, namespace
SassyDigitizing\WhopIntegration\) gets installed as a dependency —
vlucas/phpdotenv and PHPUnit are require-dev only and won't be pulled in
by consumers. The reference files (bootstrap.php, webhooks/, public/)
land in vendor/sassydigitizing/whop-integration/ for reference; copy what
you need into your own app rather than executing them from inside vendor/.
For the complete step-by-step setup (sandbox and production Whop accounts,
webhook registration, wiring into an existing app's database) see
docs/WHOP_INTEGRATION_GUIDE.md —
this README covers the library API; that guide covers the end-to-end process.
Quick start
use SassyDigitizing\WhopIntegration\Logging\Logger; use SassyDigitizing\WhopIntegration\Services\CurlHttpTransport; use SassyDigitizing\WhopIntegration\Services\WhopApiClient; use SassyDigitizing\WhopIntegration\Services\WhopWebhookVerifier; use SassyDigitizing\WhopIntegration\Services\WhopProductService; use SassyDigitizing\WhopIntegration\Services\WhopPlanService; use SassyDigitizing\WhopIntegration\Services\WhopCheckoutService; use SassyDigitizing\WhopIntegration\Services\WhopPaymentLookup; use SassyDigitizing\WhopIntegration\Services\WhopInvoiceService; $logger = new Logger('/path/to/whop.log'); $client = new WhopApiClient($apiKey, new CurlHttpTransport(), $logger); // 1. Create a product once per item/category (cache the id yourself). $products = new WhopProductService($client, $apiBase, $companyId); $product = $products->createProduct('Invoice #123'); // $product['id'] // 2. Dynamic price, no pre-made plan needed. $plans = new WhopPlanService($client, $apiBase); $plan = $plans->createOneTimePlan($product['id'], 25.50, 'usd'); // 3. Create the hosted checkout session. $checkout = new WhopCheckoutService($client, $checkoutBase); $session = $checkout->createCheckoutSession($plan['id'], $yourOrderRef, $returnUrl); header('Location: ' . $session['purchase_url']); // 4. In your webhook endpoint: verify before trusting anything. $verifier = new WhopWebhookVerifier($webhookSecret); if (!$verifier->verify($rawRequestBody, $lowercasedHeaders)) { http_response_code(401); exit; } // 5. Optional: re-confirm a specific payment server-side (e.g. on return). $lookup = new WhopPaymentLookup($client, $apiBase); $payment = $lookup->getPayment($paymentId); $isPaid = ($payment['status'] ?? null) === 'paid'; // 6. Need an itemized breakdown instead of one price? Use WhopInvoiceService // (Whop's native Invoices API) instead of steps 1-3 — it supports a real // line_items array; checkout_sessions is always exactly one plan. $invoices = new WhopInvoiceService($client, $apiBase, $companyId); $invoice = $invoices->createInvoice( lineItems: [ ['label' => 'Order #4021', 'quantity' => 1, 'unit_price' => 25.00], ['label' => 'Order #4022', 'quantity' => 1, 'unit_price' => 40.00], ], currency: 'usd', emailAddress: $customerEmail, customerName: $customerName, dueDate: new DateTimeImmutable('+14 days'), productTitle: 'Orders #4021, #4022', ); // $invoice['pay_online_url'] to send the customer; $invoice['id'] to check // status later via $invoices->getInvoice($invoice['id'])['status'].
$apiBase/$checkoutBase are just strings — point them at
https://api.whop.com/api/v1 + /api/v2 for production, or
https://sandbox-api.whop.com/api/v1 + /api/v2 for sandbox. Nothing in the
library cares which; that decision lives entirely in your own config.
Library reference
| Class | Method | Whop endpoint |
|---|---|---|
WhopWebhookVerifier |
verify(string $rawBody, array $headers): bool |
— (local HMAC check) |
WhopApiClient |
get(string $url): array / post(string $url, array $body): array |
any |
WhopProductService |
createProduct(string $title): array |
POST /products |
WhopPlanService |
createOneTimePlan(string $productId, float $amount, string $currency = 'usd'): array |
POST /plans |
WhopCheckoutService |
createCheckoutSession(string $planId, string $orderRef, string $redirectUrl): array |
POST /checkout_sessions |
WhopInvoiceService |
createInvoice(lineItems, currency, emailAddress, customerName, dueDate, productId?, productTitle?): array / getInvoice(string $invoiceId): array |
POST /invoices, GET /invoices/{id} |
WhopPaymentLookup |
getPayment(string $paymentId): array |
GET /payments/{id} |
Logging\Logger |
info(string $event, array $context = []) / error(...) |
— (redacts secret-shaped keys automatically) |
WhopApiClient throws typed exceptions you can catch individually:
WhopAuthException (401/403), WhopNotFoundException (404),
WhopValidationException (422), WhopRateLimitException (429, after
retries), WhopServerException (5xx, after retries) — all extend
WhopApiException.
Running the tests
composer install vendor/bin/phpunit
All 46 tests run against fakes — no live network calls, no database.
Reference implementation
Everything below describes the example app shipped alongside the library
(bootstrap.php, webhooks/whop.php, public/*.php) — useful to see a
complete, working, tested wiring before adapting it into your own project.
Important: no database, dummy data only
The reference app uses no database and no file storage — by design, for
this reference build specifically (your own integration should use real
persistence; see the integration guide, §5.4).
SassyDigitizing\WhopIntegration\Data\DummyOrderStore and DummyWebhookEventStore
hold orders and webhook-idempotency records in a plain PHP array:
- State lives only for the lifetime of one PHP process. Each real HTTP
request (
checkout.php,webhooks/whop.php,order-status.php) gets a fresh, empty store, so an order created bycheckout.phpwill not be visible towebhooks/whop.phpwhen Whop calls it back. - Everything is fully exercised and correct within a single process — see the PHPUnit tests, and "Try it locally" below.
- Before production: implement a real repository with the same method
signatures (
create,findByRef,findByWhopPaymentId,markPaid,markStatus/alreadyProcessed,insert,markProcessed) backed by your database, and pass instances of those intoOrderService/webhooks/whop.phpinstead. Nothing else needs to change.
Setup
cp .env.example .env
# Fill in WHOP_API_KEY, WHOP_COMPANY_ID, WHOP_WEBHOOK_SECRET, APP_BASE_URL
composer install
WHOP_COMPANY_ID must be the real biz_... ID from your dashboard URL
(whop.com/dashboard/biz_XXXXXXXX/...) — not an app_... ID.
Set WHOP_SANDBOX=true to point both api_base and checkout_base at
Whop's sandbox host in one flag — see config/whop.php. WHOP_API_BASE /
WHOP_CHECKOUT_BASE, if set, always override the flag.
Try it locally
php -S localhost:8080 -t .
SECRET="whsec_dGVzdHNlY3JldGtleWJ5dGVzMTIzNDU2" # matches .env's WHOP_WEBHOOK_SECRET BODY='{"id":"evt_1","type":"payment.succeeded","data":{"id":"pay_999","status":"paid","total":9.99,"currency":"usd","plan":{"id":"plan_demo1"},"membership":{"id":"mem_999"},"metadata":{"order_ref":"demo-order-1"}}}' ID="wh_demo1" TS=$(date +%s) KEY=$(php -r "echo base64_decode(substr('$SECRET',6));") SIG="v1,$(php -r "echo base64_encode(hash_hmac('sha256', '$ID.$TS.$BODY', '$KEY', true));")" curl -i -X POST http://localhost:8080/webhooks/whop.php \ -H "webhook-id: $ID" -H "webhook-timestamp: $TS" -H "webhook-signature: $SIG" \ -H "Content-Type: application/json" -d "$BODY"
Expect 200 {"status":"ok"}. Check logs/whop.log for the structured log line.
For real end-to-end testing against Whop, expose this endpoint via
ngrok http 8080, register that URL plus the events in
docs/WHOP_INTEGRATION_GUIDE.md §3.6 in the Whop dashboard, and use Whop's
webhook test-send feature or a real sandbox payment.
How checkout works
Two ways to call public/checkout.php:
Fixed price: ?plan_id=plan_xxx&product_id=prod_xxx&email=buyer@example.com
Dynamic price: ?product_id=prod_xxx&amount=25.50&email=buyer@example.com
(optional ¤cy=usd). Creates a one-time-price plan on the fly via
WhopPlanService, then proceeds like the fixed-price path.
Either way: creates a local (dummy) PENDING order, calls the Whop API to
create a checkout session, redirects to purchase_url. Whop redirects back
to checkout-return.php after payment — that page never treats the
redirect itself as proof of payment; only a verified payment.succeeded
webhook (or, for the return page's own UX, a direct server-side
WhopPaymentLookup re-check) does.
Files
config/whop.php Env-driven config (API key, secrets, URLs)
bootstrap.php Loads .env, exposes $config
src/Logging/Logger.php Structured JSON logger, redacts secrets
src/Services/Whop*.php The library — see "Library reference" above
src/Services/OrderService.php Reference fulfillment orchestration (one-time only)
src/Services/AccessGrant.php Seam for granting/revoking product access
src/Mail/ConfirmationMailer.php Payment confirmation email
src/Data/DummyOrderStore.php PLACEHOLDER order storage (see above)
src/Data/DummyWebhookEventStore.php PLACEHOLDER webhook idempotency (see above)
webhooks/whop.php POST /webhooks/whop.php endpoint
public/checkout.php Starts checkout, redirects to Whop
public/checkout-return.php Post-payment holding page (server-side re-verify)
public/order-status.php Read-only order status JSON endpoint
public/invoices.php, invoice.php Standalone localStorage-backed demo invoice UI
public/ensure-product.php AJAX endpoint: create-or-reuse a Whop product
tests/ PHPUnit — verifier, HTTP client, services,
order service, full webhook endpoint tests
Security notes
- Webhook signature verification uses the raw request body,
hash_equals(constant-time), and rejects timestamps more than 5 minutes old — seeWhopWebhookVerifier. WHOP_API_KEY/WHOP_WEBHOOK_SECRETare never logged (Loggerredacts any context key containingsecret,password,signature,api_key,authorization).- Order fulfillment only happens after signature verification AND a plan-id
match between the webhook payload and the locally stored pending order —
see
OrderService::handlePaymentSucceeded. .envis git-ignored; only.env.example(placeholders) is committed — verify this every time before pushing; it's the easiest way to leak a live key.
License
MIT — see LICENSE.