Run one dedicated PHP reference backend for one provider/account/environment. It receives native provider notifications and fulfills them through the existing KDPika API. It does not change KDPika's own checkout, billing processor, database schema or SDK.
What you will learn
- Deploy safely on PHP shared hosting without Docker, Node, root or a daemon.
- Configure private credentials, approved offer mapping and durable state.
- Run bounded work/mail cron jobs and review uncertain results.
Prerequisites
PHP 8.2+ with curl, pdo_sqlite, mbstring and openssl; a private persistent POSIX filesystem; verified HTTPS; and hosting cron. Confirm the same extensions in web PHP and CLI PHP. SQLite files belong to this developer reference, not KDPika's MySQL schema. Read the generic trust boundary and select the provider guide before configuring notifications.
Download the complete source archive and source manifest. All source modules and deliberately synthetic fixtures are included; no credentials, state, generated binaries or buyer keys are packaged.
Choose your provider
Install the files
Use a dedicated subdomain/backend directory, not your store's existing document root. Replace ACCOUNT and /home/ACCOUNT/license-events with your actual hosting account/path. A suitable layout is:
/home/ACCOUNT/license-events/
public/ dedicated HTTPS document root
index.php copied from stores/public/index.php
.htaccess copied from stores/public/.htaccess
private/kdpika-store/ entire archive extracted here
stores/
fulfillment/
environment.php copy stores/environment.example.php
config.json copy stores/config.example.json
fulfillment.json copy fulfillment/config.example.json
state/ created privately by the worker
Keep private directories 0700 and configuration/files 0600. The public bootstrap resolves its sibling private/kdpika-store and loads no credentials from a URL. Only copy the two public bootstrap files into this dedicated document root. Do not upload the entire archive or its private state into public_html. Never overwrite an existing store's index.php or .htaccess.
Map the purchase to a product plan
- In private config.json select the provider and exact environment sandbox. Set seller_id and provider-specific fields using its guide. Remove no validation by changing source.
- Map the verified provider price/variant/product reference to an internal offer in offer_map. These mapping keys need not have the same format; they are deliberately two independent boundaries.
- In private fulfillment.json set source to the exact provider string, seller_id to the same stable merchant identity, environment to sandbox, base_url to https://licensing.kdpika.com and product_id to your product.
- Under offers map that internal offer to your real KDPika plan_id and explicitly chosen renewal_days. Use a fixed-duration plan when selling limited periods; a perpetual initial plan will not become a timed subscription merely because renewal_days is present.
- For this PHP reference add create_idempotency with value effect_id to fulfillment.json. This retains the literal Stripe Session/Event ID for its initial create. It is a PHP reference option, not a public KDPika API setting or a feature of the other three generic runtimes.
{
"provider": "stripe",
"environment": "sandbox",
"seller_id": "YOUR_STORE_ACCOUNT",
"shop_domain": "YOUR_SHOP.myshopify.com",
"webhook_id": "YOUR_PAYPAL_WEBHOOK_ID",
"api_version": "2025-07-30.basil",
"offer_map": {
"YOUR_PROVIDER_PRICE_OR_VARIANT_ID": "YOUR_VERIFIED_PRICE_ID"
}
}
The shared fulfillment configuration defines product, plan and renewal behavior. Initial creation uses the stored plan duration; renewal adds integer days, not an exact provider calendar end. There is no automatic in-place plan migration, proration or multi-line allocation.
Load private credentials
Copy this example to environment.php at the private archive root. Replace every YOUR_ value using private hosting configuration. Generate a separate unpredictable command secret and a base64 encoding of 32 random bytes for the encrypted key outbox. Keep an encrypted backup of the outbox key separately from state; losing it prevents saved-key delivery.
<?php
declare(strict_types=1);
// Copy to the PRIVATE archive root as environment.php; chmod0600 outside web root.
// Replace every YOUR_ value. Never upload this file into public_html or commit it.
$private = __DIR__;
return [
'KDP_STORE_CONFIG' => $private . '/config.json',
'KDP_STORE_STATE' => $private . '/state/store',
'KDP_STORE_WEBHOOK_SECRET' => 'YOUR_PROVIDER_ENDPOINT_SIGNING_SECRET',
'KDP_STORE_API_KEY' => 'YOUR_PROVIDER_API_KEY',
'KDP_STORE_API_SECRET' => 'YOUR_PROVIDER_API_SECRET_IF_BASIC_AUTH',
'KDP_STORE_CLIENT_ID' => 'YOUR_PAYPAL_CLIENT_ID_IF_PAYPAL',
'KDP_STORE_CLIENT_SECRET' => 'YOUR_PAYPAL_CLIENT_SECRET_IF_PAYPAL',
'KDP_FULFILLMENT_CONFIG' => $private . '/fulfillment.json',
'KDP_FULFILLMENT_STATE' => $private . '/state/fulfillment',
'KDP_FULFILLMENT_SECRET' => 'YOUR_SEPARATE_UNPREDICTABLE_COMMAND_SECRET',
'KDP_FULFILLMENT_OUTBOX_KEY' => 'YOUR_BASE64_ENCODED_32_RANDOM_BYTES',
'KDP_API_KEY' => 'YOUR_PRODUCT_SCOPED_SANDBOX_API_KEY',
'KDP_SMTP_HOST' => 'YOUR_SMTP_HOST',
'KDP_SMTP_PORT' => '587',
'KDP_SMTP_USERNAME' => 'YOUR_SMTP_USERNAME',
'KDP_SMTP_PASSWORD' => 'YOUR_SMTP_PASSWORD',
'KDP_SMTP_FROM' => 'support@example.invalid',
'KDP_SOFTWARE_NAME' => 'Example application',
];
Provider API keys are not the product-scoped KDP_API_KEY. Keep the provider destination secret separate from KDP_FULFILLMENT_SECRET and outbound LMP webhook secrets. For PayPal automatic OAuth set KDP_STORE_API_KEY to an empty string and fill its private client ID/secret. Remove unused provider-specific secrets from your private copy rather than replacing them with other accounts' credentials.
Configuration is fingerprint-bound to existing ledgers. Review migrations before changing account, source, mapping or outbox key. Rotating an API credential is not permission to delete state. Production must not contain KDP_STORE_TEST_API, KDP_STORE_ALLOW_LOOPBACK, KDP_ALLOW_LOOPBACK or KDP_SMTP_ALLOW_LOOPBACK test overrides.
Run the private cron jobs
Check your hosting panel's PHP CLI path and version. A typical cPanel command is below; /usr/local/bin/php is an example, not a guarantee for every host. Some hosts use /opt/cpanel/ea-php82/root/usr/bin/php or another versioned binary. Select the same supported version/extensions used by web PHP.
/usr/local/bin/php /home/ACCOUNT/license-events/private/kdpika-store/stores/cron.php inspect
Expected: safe aggregate states JSON, never a full key. Configure these two commands every minute in Cron Jobs:
/usr/local/bin/php /home/ACCOUNT/license-events/private/kdpika-store/stores/cron.php work
/usr/local/bin/php /home/ACCOUNT/license-events/private/kdpika-store/stores/cron.php mail
They are separate cron entries with schedule * * * * *. Do not create a public HTTP cron URL. work processes at most 20 inbox events per run; its shared licensing worker processes at most 25 effects and paces writes. mail processes at most 25 new saved keys. Overlapping workers use nonblocking private locks; inspect safe failures rather than disabling locking. Configure private cron failure notifications with your host.
Receipt, work and recovery
POST /provider-event is the only public route. A valid durable receipt returns HTTP 202 and accepted:true with duplicate:false, or duplicate:true for an unchanged delivery. Invalid proof, conflict, excess body and overload do not return successful receipt. The whole-service cap is 60 incoming requests/minute and raw body size is at most 262144 bytes. This basic cap is not a substitute for hosting firewall/edge controls.
Provider reads use fixed HTTPS origins, verified TLS, bounded response sizes and no redirects. The optional Node ingress and Python ingress preserve raw bytes and only acknowledge after the PHP service commits. The direct PHP path requires neither relay.
The worker stores native decisions and stable commands before dispatch. A paid receipt is applied once; a missing initial mapping, changed recipient/offer, partial refund or uncertain remote result is held for review. A full refund before known issuance leaves an adverse-reference record, so a later matching paid notification is not blindly granted. This is conservative reconciliation, not universal causal ordering or cross-system exactly-once delivery.
If inspect reports REVIEW or IN_FLIGHT, check the private provider ledger, current provider objects, licensing effects and dashboard before another write. No automatic reset/reissue command is supplied. Preserve backups and stable business identities. Do not manually delete a failed row, invent a new order or rerun a possibly successful additive renewal.
Deliver the saved key
Use the SMTP outbox guide. Issuance and delivery have separate durable acknowledgements. A saved key can be exported through the PHP fulfillment reference for an authenticated recipient, without creating another license.
Source and verification
The exact sources were run against synthetic provider contracts and an actual isolated licensing API. Local contracts are not real merchant account purchases or production host deployment. Run the chosen provider's Test procedure, real HTTPS delivery and SMTP inbox test on your backend before Live use. Native Windows ACLs/locks are not claimed; use a POSIX backend or implement and verify an appropriate private filesystem/locking adapter.
Retention and support
Raw inboxes and saved decisions may contain buyer metadata. Keep them private, restrict operator access, document your own retention/deletion process and encrypt backups. Do not automatically delete the durable business-effect identities needed to prevent duplicate issuance; separate minimized identifiers from raw PII when implementing a reviewed retention migration. No automatic PII-retention policy is invented by this example. Never send SQLite files, raw events, credentials or full keys in public support reports.
Expected result
Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies.
Common mistakes
Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker.
Security notes
Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed.
Troubleshooting
For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly.
Next steps
Run the selected provider Sandbox procedure, then test private key email delivery. Review supported recipe limitations before enabling Live.