---
title: "Configuration — Purchases for Laravel | Roundly"
description: "Every config key and env variable — models, providers, audit, queue, webhook routes and Apple, Google and Stripe credentials."
url: https://roundly-consulting.com/open-source/docs/purchases-for-laravel/configuration
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Purchases for Laravel](https://roundly-consulting.com/open-source/docs/purchases-for-laravel.md)

# Configuration

The package works with zero host configuration, though no provider can verify anything until its credentials are set. The published config/purchases.php in full:

```php
/*
 | Every on/off switch below is read from the environment as a string, so each is
 | read strictly: 1/true/on/yes are on, 0/false/off/no are off, unset or blank
 | (KEY=) keeps the documented default (for push authentication that is ON —
 | fail-closed), and anything else throws an InvalidConfigurationException naming
 | the key. Every other setting is strict too: blank means not set, so the default
 | applies, while a duration such as the Stripe tolerance must be a whole number
 | and URLs and queue names must be strings.
 */
return [
    'models' => [
        'purchase' => \RoundlyConsulting\Purchases\Models\Purchase::class,
        'purchase-item' => \RoundlyConsulting\Purchases\Models\PurchaseItem::class,
        'purchase-refund' => \RoundlyConsulting\Purchases\Models\PurchaseRefund::class,
        'purchase-notification' => \RoundlyConsulting\Purchases\Models\PurchaseNotification::class,
        'subscription' => \RoundlyConsulting\Purchases\Models\Subscription::class,
        'subscription-item' => \RoundlyConsulting\Purchases\Models\SubscriptionItem::class,
    ],

    'providers' => [
        \RoundlyConsulting\Purchases\Providers\Apple\Apple::class,
        \RoundlyConsulting\Purchases\Providers\Google\Google::class,
        \RoundlyConsulting\Purchases\Providers\Stripe\Stripe::class,
    ],

    // Owner morph key type: "bigint", "uuid" or "ulid" — anything else throws.
    'key_type' => env('PURCHASES_KEY_TYPE', 'bigint'),

    // Log every verified raw payload to purchase_notifications before reducing it.
    'audit' => [
        'enabled' => env('PURCHASES_AUDIT_ENABLED', true),
    ],

    // Persist verified notifications on a queue; the webhook still 204s immediately.
    'queue' => [
        'enabled' => env('PURCHASES_QUEUE_ENABLED', false),
        'connection' => env('PURCHASES_QUEUE_CONNECTION'),
        'queue' => env('PURCHASES_QUEUE_NAME'),
    ],

    'routes' => [
        'enabled' => env('PURCHASES_ROUTES_ENABLED', false),
        'prefix' => env('PURCHASES_ROUTES_PREFIX', 'purchases'),
        'middleware' => ['api'],
    ],

    'settings' => [
        'apple' => [
            // The one App Store app this host accepts notifications and transactions for.
            'bundle_id' => env('PURCHASES_APPLE_BUNDLE_ID'),
            'app_apple_id' => env('PURCHASES_APPLE_APP_APPLE_ID'),

            // Production unless switched on.
            'sandbox' => env('PURCHASES_APPLE_SANDBOX', false),
            'url' => [
                'live' => env('PURCHASES_APPLE_LIVE_URL', 'https://buy.itunes.apple.com'),
                'sandbox' => env('PURCHASES_APPLE_SANDBOX_URL', 'https://sandbox.itunes.apple.com'),
            ],
            'password' => env('PURCHASES_APPLE_PASSWORD'),
            'certificate_clock_skew' => env('PURCHASES_APPLE_CERTIFICATE_CLOCK_SKEW', 60),
            'api' => [
                'key_id' => env('PURCHASES_APPLE_KEY_ID'),
                'issuer_id' => env('PURCHASES_APPLE_ISSUER_ID'),
                'private_key' => env('PURCHASES_APPLE_PRIVATE_KEY'),
                'url' => [
                    'live' => env('PURCHASES_APPLE_API_LIVE_URL', 'https://api.storekit.itunes.apple.com'),
                    'sandbox' => env('PURCHASES_APPLE_API_SANDBOX_URL', 'https://api.storekit-sandbox.itunes.apple.com'),
                ],
            ],
        ],

        'google' => [
            'package_name' => env('PURCHASES_GOOGLE_PACKAGE_NAME'),
            'service_account' => [
                'client_email' => env('PURCHASES_GOOGLE_CLIENT_EMAIL'),
                'private_key' => env('PURCHASES_GOOGLE_PRIVATE_KEY'),
                'token_uri' => env('PURCHASES_GOOGLE_TOKEN_URI', 'https://oauth2.googleapis.com/token'),
            ],
            'base_url' => env('PURCHASES_GOOGLE_BASE_URL', 'https://androidpublisher.googleapis.com'),
            'acknowledge' => env('PURCHASES_GOOGLE_ACKNOWLEDGE', true),

            // Pub/Sub pushes are authenticated before they are read — fail-closed.
            'push' => [
                'authenticate' => env('PURCHASES_GOOGLE_PUSH_AUTHENTICATE', true),
                'audience' => env('PURCHASES_GOOGLE_PUSH_AUDIENCE'),
                'service_account_email' => env('PURCHASES_GOOGLE_PUSH_SERVICE_ACCOUNT'),
                'token' => env('PURCHASES_GOOGLE_PUSH_TOKEN'),
                'jwks_url' => env('PURCHASES_GOOGLE_PUSH_JWKS_URL', 'https://www.googleapis.com/oauth2/v3/certs'),
                'jwks_cache_ttl' => env('PURCHASES_GOOGLE_PUSH_JWKS_CACHE_TTL', 3600),
            ],
        ],

        'stripe' => [
            'secret' => env('PURCHASES_STRIPE_SECRET'),
            'webhook_secret' => env('PURCHASES_STRIPE_WEBHOOK_SECRET'),
            'api_version' => env('PURCHASES_STRIPE_API_VERSION', '2026-05-27.dahlia'),
            'base_url' => env('PURCHASES_STRIPE_BASE_URL', 'https://api.stripe.com/v1'),
            'tolerance' => env('PURCHASES_STRIPE_TOLERANCE', 300),
        ],
    ],
];
```

## Package keys

| Key | Default | Purpose |
| --- | --- | --- |
| `models.*` | package models | Eloquent model per record type — swap in your own subclass; any other class throws. |
| `providers` | Apple, Google, Stripe | Registered provider classes, resolved by their id(). Must be a list of classes implementing Provider. |
| `key_type` | bigint | Owner morph key type: bigint, uuid or ulid; anything else throws when the migrations run. |
| `audit.enabled` | true | Log every verified payload to purchase\_notifications. |
| `queue.enabled` | false | Verify synchronously, persist on a queue. |
| `queue.connection` | null | Queue connection for async recording; unset or blank = the default connection. |
| `queue.queue` | null | Queue name for async recording; unset or blank = the default queue. |
| `routes.enabled` | false | Register the bundled webhook route. Only an explicit on counts; a switch that is not set keeps it off. |
| `routes.prefix` | purchases | URI prefix of the webhook route. |
| `routes.middleware` | \['api'\] | Middleware applied to the webhook route — a list of names. |

## Apple — settings.apple

| Key | Env | Default |
| --- | --- | --- |
| `bundle_id` | `PURCHASES_APPLE_BUNDLE_ID` | — |
| `app_apple_id` | `PURCHASES_APPLE_APP_APPLE_ID` | — |
| `sandbox` | `PURCHASES_APPLE_SANDBOX` | false |
| `url.live` | `PURCHASES_APPLE_LIVE_URL` | buy.itunes.apple.com |
| `url.sandbox` | `PURCHASES_APPLE_SANDBOX_URL` | sandbox.itunes.apple.com |
| `password` | `PURCHASES_APPLE_PASSWORD` | — |
| `certificate_clock_skew` | `PURCHASES_APPLE_CERTIFICATE_CLOCK_SKEW` | 60 |
| `api.key_id` | `PURCHASES_APPLE_KEY_ID` | — |
| `api.issuer_id` | `PURCHASES_APPLE_ISSUER_ID` | — |
| `api.private_key` | `PURCHASES_APPLE_PRIVATE_KEY` | — |
| `api.url.live` | `PURCHASES_APPLE_API_LIVE_URL` | api.storekit.itunes.apple.com |
| `api.url.sandbox` | `PURCHASES_APPLE_API_SANDBOX_URL` | api.storekit-sandbox.itunes.apple.com |

bundle\_id is required for Apple: every App Store notification — and every transaction the App Store Server API returns — must name it, or it is rejected, and it also signs App Store Server API requests. app\_apple\_id (App Store Connect → App Information) is required in production, where a notification must carry it. sandbox is false by default — production; set it to true only on a host that receives Sandbox notifications, which also switches verifyReceipt and the App Store Server API to their sandbox hosts. api.\* enables the App Store Server API, the modern replacement for verifyReceipt; password is the shared secret for legacy receipts. certificate\_clock\_skew is the leeway in seconds (0–3600) applied to both ends of every certificate’s validity window — a value outside that range throws InvalidConfigurationException, so a typo can never switch the check off, while a blank value is not set, so 60 applies.

## Google — settings.google

| Key | Env | Default |
| --- | --- | --- |
| `package_name` | `PURCHASES_GOOGLE_PACKAGE_NAME` | — |
| `service_account.client_email` | `PURCHASES_GOOGLE_CLIENT_EMAIL` | — |
| `service_account.private_key` | `PURCHASES_GOOGLE_PRIVATE_KEY` | — |
| `service_account.token_uri` | `PURCHASES_GOOGLE_TOKEN_URI` | oauth2.googleapis.com/token |
| `base_url` | `PURCHASES_GOOGLE_BASE_URL` | androidpublisher.googleapis.com |
| `acknowledge` | `PURCHASES_GOOGLE_ACKNOWLEDGE` | true |
| `push.authenticate` | `PURCHASES_GOOGLE_PUSH_AUTHENTICATE` | true |
| `push.audience` | `PURCHASES_GOOGLE_PUSH_AUDIENCE` | — |
| `push.service_account_email` | `PURCHASES_GOOGLE_PUSH_SERVICE_ACCOUNT` | — |
| `push.token` | `PURCHASES_GOOGLE_PUSH_TOKEN` | — |
| `push.jwks_url` | `PURCHASES_GOOGLE_PUSH_JWKS_URL` | www.googleapis.com/oauth2/v3/certs |
| `push.jwks_cache_ttl` | `PURCHASES_GOOGLE_PUSH_JWKS_CACHE_TTL` | 3600 |

acknowledge controls whether verified purchases are acknowledged with Google Play automatically. push.\* authenticates real-time developer notifications and is fail-closed — see Google Play. purchases:install --providers appends its three keys, and purchases:providers reports Google as missing config until one mechanism is set: a push token, the OIDC audience and service-account pair, or push.authenticate off. package\_name also binds RTDNs to your app: an authenticated notification for another package — on a shared Pub/Sub topic — is audited and answered 204, never applied. push.jwks\_cache\_ttl, the cache lifetime of Google’s signing keys in seconds, must be at least 1.

## Stripe — settings.stripe

| Key | Env | Default |
| --- | --- | --- |
| `secret` | `PURCHASES_STRIPE_SECRET` | — |
| `webhook_secret` | `PURCHASES_STRIPE_WEBHOOK_SECRET` | — |
| `api_version` | `PURCHASES_STRIPE_API_VERSION` | 2026-05-27.dahlia |
| `base_url` | `PURCHASES_STRIPE_BASE_URL` | api.stripe.com/v1 |
| `tolerance` | `PURCHASES_STRIPE_TOLERANCE` | 300 |

tolerance is the webhook timestamp tolerance in seconds — at least 1, default 300 — so the replay window can’t be switched off by a typo; api\_version is sent as the Stripe-Version header on every REST call. Set both keys: webhook\_secret verifies every event, and secret is needed for webhooks too on API versions since 2025-03-31 (the pinned one included) — a payment\_intent.\* event names no invoice and a one-off invoice.paid names no PaymentIntent, so the package asks Stripe’s Invoice Payments API (see Stripe). Without secret such an event is refused, and purchases:providers reports Stripe as missing config until both keys are set.

## Environment

Every credential is env-driven, so you rarely publish the config at all. The config file passes env values through raw, and the package reads them strictly. The on/off switches — PURCHASES\_AUDIT\_ENABLED, PURCHASES\_QUEUE\_ENABLED, PURCHASES\_ROUTES\_ENABLED, PURCHASES\_APPLE\_SANDBOX, PURCHASES\_GOOGLE\_ACKNOWLEDGE and PURCHASES\_GOOGLE\_PUSH\_AUTHENTICATE — accept true/false, 1/0, on/off or yes/no; unset or blank (KEY=) keeps the documented default (for push authentication that is on — fail-closed). Anything else, like a PURCHASES\_KEY\_TYPE outside bigint, uuid and ulid, throws an InvalidConfigurationException naming the key — a typo never quietly becomes the default:

```bash
# Apple — your app, App Store Server API (+ legacy verifyReceipt shared secret)
PURCHASES_APPLE_BUNDLE_ID=com.example.app
PURCHASES_APPLE_APP_APPLE_ID=1234567890
PURCHASES_APPLE_SANDBOX=false
PURCHASES_APPLE_KEY_ID=
PURCHASES_APPLE_ISSUER_ID=
PURCHASES_APPLE_PRIVATE_KEY=
PURCHASES_APPLE_PASSWORD=
PURCHASES_APPLE_CERTIFICATE_CLOCK_SKEW=60

# Google Play — service account
PURCHASES_GOOGLE_PACKAGE_NAME=
PURCHASES_GOOGLE_CLIENT_EMAIL=
PURCHASES_GOOGLE_PRIVATE_KEY=
PURCHASES_GOOGLE_ACKNOWLEDGE=true
PURCHASES_GOOGLE_PUSH_AUDIENCE=https://your-app.test/purchases/webhooks/google
PURCHASES_GOOGLE_PUSH_SERVICE_ACCOUNT=rtdn-push@my-project.iam.gserviceaccount.com

# Stripe
PURCHASES_STRIPE_SECRET=
PURCHASES_STRIPE_WEBHOOK_SECRET=
PURCHASES_STRIPE_TOLERANCE=300

# Package behaviour
PURCHASES_KEY_TYPE=bigint
PURCHASES_AUDIT_ENABLED=true
PURCHASES_QUEUE_ENABLED=false
PURCHASES_ROUTES_ENABLED=false
PURCHASES_ROUTES_PREFIX=purchases
```

## Strict settings

The other settings are just as strict. A variable that is not set — left out, or blank such as PURCHASES\_STRIPE\_TOLERANCE= — takes its default; one you set to the wrong shape throws RoundlyConsulting\\Purchases\\Exceptions\\InvalidConfigurationException naming the key:

- Durations — PURCHASES\_STRIPE\_TOLERANCE, PURCHASES\_GOOGLE\_PUSH\_JWKS\_CACHE\_TTL and PURCHASES\_APPLE\_CERTIFICATE\_CLOCK\_SKEW — take a whole number such as 300; five or 300.5 throw instead of becoming 0.
- URLs, the API version, the route prefix and the queue connection and name must be strings. Leave the queue ones unset or blank for the default connection and queue.
- purchases.providers and purchases.routes.middleware must be lists, and every provider must implement RoundlyConsulting\\Purchases\\Providers\\Provider.
- A blank credential (PURCHASES\_GOOGLE\_PUSH\_TOKEN=) is not set, so it reads as not configured; a non-string one throws.

Provider secrets are read only from config and env, are marked #\[SensitiveParameter\] so they never leak into stack traces, and are never logged. php artisan about reports them only as SET or MISSING, and a broken duration or provider list as INVALID.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/purchases-for-laravel.md)
