Search by

sassydigitizing / whop-integration

shamamabinshakil.007@gmail.com

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

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.1.0 2026-08-22 00:41 UTC

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 by checkout.php will not be visible to webhooks/whop.php when 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 into OrderService / webhooks/whop.php instead. 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 &currency=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 — see WhopWebhookVerifier.
  • WHOP_API_KEY / WHOP_WEBHOOK_SECRET are never logged (Logger redacts any context key containing secret, 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.
  • .env is 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.