Warning
This package is in active development and is not yet ready for production use.
The public API may change at any time before version 1.0.0 is released.
Please do not depend on it in production projects until a stable release is published.
Symfony bundle for the Whop PHP SDK —
autowires the WhopApiClient and WebhookVerifier from configuration and ships an
overridable, bundle-owned webhook controller.
- PHP 8.4+
- Symfony 7
composer require devmatchable/whop-symfony-bundleIf Symfony Flex is installed, the recipe registers the bundle, drops the config and route files, and appends the required environment variables automatically. Otherwise, follow the manual setup below.
The bundle is configured under the whop key, conventionally in
config/packages/whop.yaml:
whop:
api_key: '%env(WHOP_API_KEY)%' # required
webhook_secret: '%env(WHOP_WEBHOOK_SECRET)%' # required
base_url: 'https://api.whop.com/api/v1' # optional, default shown
http_client: null # optional, default shown
webhook_path: '/_whop/webhook' # optional, default shownapi_key(required) — the Whop API key (Bearer token) theWhopApiClientauthenticates with.webhook_secret(required) — the Standard Webhooks signing secret theWebhookVerifierchecks incoming webhooks against. Supports bothwhsec_(production) andws_(sandbox) secret formats.base_url(optional) — the Whop API base URL. Override with the sandbox URL (https://sandbox-api.whop.com/api/v1) for non-production environments.http_client(optional) — the service id of a PSR-18 / Symfony HTTP client to use for outbound API calls. Whennull, the bundle uses the default Symfony HTTP client.webhook_path(optional) — the path the bundle's webhook route is mounted at. Point your Whop webhook configuration at this path.
-
Register the bundle in
config/bundles.php:return [ // ... Matchable\Whop\Bundle\WhopBundle::class => ['all' => true], ];
-
Add
config/packages/whop.yamlwith the configuration block shown above. -
Import the bundle's webhook route in
config/routes/whop.yaml:# config/routes/whop.yaml whop: resource: '@WhopBundle/config/routes.php'
The SDK's WhopApiClient is registered as an autowired service. Type-hint it in any
service constructor:
use Matchable\Whop\WhopApiClient;
final readonly class PaymentLookup
{
public function __construct(
private WhopApiClient $whop,
) {
}
public function fetch(string $paymentId): void
{
$payment = $this->whop->payments->get($paymentId);
// ...
}
}The WebhookVerifier is autowired the same way if you need to verify webhooks
outside the bundle's controller.
Point your Whop webhook configuration at the bundle's webhook path (default
/_whop/webhook). The bundle-owned controller verifies the Standard Webhooks
signature, decodes the JSON body, and hands off to a WhopWebhookHandlerInterface.
The default handler dispatches a WhopWebhookReceivedEvent, so zero-config works:
just subscribe to the event.
use Matchable\Whop\Bundle\Event\WhopWebhookReceivedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener]
final class WhopWebhookListener
{
public function __invoke(WhopWebhookReceivedEvent $event): void
{
// $event->payload — decoded webhook JSON (array<string, mixed>)
// $event->rawPayload — the verified raw request body
}
}The controller responds with 204 No Content on success, 401 Unauthorized on an
invalid signature, and 400 Bad Request on a body that is not decodable JSON.
The webhook handler is overridable with standard Symfony service configuration — no compiler pass, no tag. Three documented paths:
-
Implement the interface and alias
WhopWebhookHandlerInterfaceto your service:# config/services.yaml services: App\Whop\MyWebhookHandler: ~ Matchable\Whop\Bundle\Webhook\WhopWebhookHandlerInterface: alias: App\Whop\MyWebhookHandler
-
Extend
EventDispatchingWebhookHandlerand overridehandle(). -
Decorate the default handler with
#[AsDecorator]to wrap it (e.g. log, then delegate to the inner handler).
The SDK's DTOs (Payment, WebhookResponse, etc.) are sealed boundary types, and
the SDK has no DTO for incoming webhook event payloads — WebhookResponse models
the response from creating a webhook registration, not an event body. The handler
therefore receives the decoded array<string, mixed> $payload. Map it to your own
domain type inside your listener or handler.
MIT — see LICENSE.