---
title: "Store service: durable worker source"
description: "Store service: durable worker source for the complete private reference application."
slug: "/docs/v1/guides/store-service/worker"
section: "Webhooks and integrations"
order: 791
audience: "all"
level: "advanced"
last_updated: "2026-10-18"
version: "v1"
related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"]
draft: false
---
This is the complete PHP service used by every provider guide. It requires the other exact archive files and private configuration from [installation](/docs/v1/guides/store-service); copying this file alone is not a deployment.

## What you will learn

Read and run the exact Store service: durable worker source with the complete shared application, without exposing private credentials or claiming an unverified payment.

## Complete source

```php
<?php
declare(strict_types=1);
// Durable store ingress/worker for developers' backends; NOT KDPika platform billing.
// PHP 8.2+, cURL, PDO SQLite, mbstring, OpenSSL; POSIX state/locks, private env.
// Local: php -S 127.0.0.1:8116 stores/service.php. Cron: php stores/service.php work
// Expected: safe durable receipt or aggregate worker state. No full keys in output.
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/http.php';
require_once __DIR__ . '/normalize.php';
define('KDP_FULFILLMENT_LIBRARY_ONLY', true);
require_once dirname(__DIR__) . '/fulfillment/php/server.php';

final class StoreService
{
    private PDO $db;
    private PDO $fulfillmentDb;
    private FulfillmentReference $fulfillment;
    private array $config;
    private array $fulfillmentConfig;
    private Closure $remote;
    private string $directory;

    public function __construct()
    {
        umask(0077);
        $file = (string) getenv('KDP_STORE_CONFIG');
        storeRequire(str_starts_with($file, '/'), 'PRIVATE_STORE_CONFIG_REQUIRED');
        $this->config = json_decode((string) file_get_contents($file), true, 64, JSON_THROW_ON_ERROR);
        storeRequire(in_array($this->config['provider'] ?? '', ['stripe', 'paddle', 'lemon-squeezy', 'gumroad', 'woocommerce', 'shopify', 'paypal', 'fastspring'], true), 'INVALID_PROVIDER');
        storeRequire(in_array($this->config['environment'] ?? '', ['sandbox', 'live'], true), 'INVALID_ENVIRONMENT');
        storeRequire($this->config['environment'] !== 'live' || getenv('KDP_CONFIRM_LIVE') === '1', 'LIVE_CONFIRMATION_REQUIRED');
        storeRequire(is_string($this->config['seller_id'] ?? null) && $this->config['seller_id'] !== '' && is_array($this->config['offer_map'] ?? null) && $this->config['offer_map'] !== [], 'PRIVATE_MAPPING_REQUIRED');
        storeRequire(($this->config['provider'] !== 'stripe') || ($this->config['api_version'] ?? '') === '2025-07-30.basil', 'PINNED_STRIPE_VERSION_REQUIRED');
        $this->fulfillmentConfig = json_decode((string) file_get_contents((string) getenv('KDP_FULFILLMENT_CONFIG')), true, 64, JSON_THROW_ON_ERROR);
        storeRequire($this->fulfillmentConfig['environment'] === $this->config['environment'] && $this->fulfillmentConfig['source'] === $this->config['provider'], 'FULFILLMENT_MAPPING_MISMATCH');
        foreach ($this->config['offer_map'] as $offer) { storeRequire(isset($this->fulfillmentConfig['offers'][$offer]), 'UNMAPPED_FULFILLMENT_OFFER'); }
        $this->directory = (string) getenv('KDP_STORE_STATE');
        storeRequire(str_starts_with($this->directory, '/') && !is_link($this->directory), 'PRIVATE_STORE_STATE_REQUIRED');
        if (!is_dir($this->directory)) { mkdir($this->directory, 0700, true); }
        storeRequire((fileperms($this->directory) & 0077) === 0, 'PRIVATE_STATE_PERMISSIONS_REQUIRED');
        $database = $this->directory . '/store.sqlite';
        storeRequire(!is_link($database), 'INVALID_STATE_FILE');
        $this->db = new PDO('sqlite:' . $database, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
        chmod($database, 0600);
        $this->db->exec("PRAGMA busy_timeout=3000;
            CREATE TABLE IF NOT EXISTS inbox(id TEXT PRIMARY KEY,body_hash TEXT NOT NULL,native TEXT NOT NULL,headers TEXT NOT NULL,state TEXT NOT NULL,decisions TEXT,received_at INTEGER NOT NULL,error_code TEXT);
            CREATE TABLE IF NOT EXISTS links(reference TEXT PRIMARY KEY,order_id TEXT NOT NULL);
            CREATE TABLE IF NOT EXISTS unresolved_denials(reference TEXT PRIMARY KEY,state TEXT NOT NULL);
            CREATE TABLE IF NOT EXISTS payments(order_id TEXT NOT NULL,receipt TEXT NOT NULL,PRIMARY KEY(order_id,receipt));
            CREATE TABLE IF NOT EXISTS commands(id TEXT PRIMARY KEY,inbox_id TEXT NOT NULL,position INTEGER NOT NULL,body TEXT NOT NULL,order_id TEXT NOT NULL,receipt TEXT NOT NULL,state TEXT NOT NULL);
            CREATE TABLE IF NOT EXISTS binding(id INTEGER PRIMARY KEY CHECK(id=1),config_hash TEXT NOT NULL);
            CREATE TABLE IF NOT EXISTS ingress(window INTEGER PRIMARY KEY,requests INTEGER NOT NULL);");
        $hash = hash('sha256', commandJson($this->config));
        $this->run('INSERT OR IGNORE INTO binding(id,config_hash) VALUES(1,?)', [$hash]);
        storeRequire($this->one('SELECT config_hash FROM binding WHERE id=1')['config_hash'] === $hash, 'CONFIGURATION_CHANGED_RECONCILE_FIRST');
        $this->fulfillment = new FulfillmentReference();
        $this->fulfillmentDb = new PDO('sqlite:' . rtrim((string) getenv('KDP_FULFILLMENT_STATE'), '/') . '/fulfillment.sqlite', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
        $this->fulfillmentDb->exec('PRAGMA busy_timeout=3000');
        $this->remote = storeApi($this->config);
    }

    private function run(string $sql, array $values = []): PDOStatement { $statement = $this->db->prepare($sql); $statement->execute($values); return $statement; }
    private function one(string $sql, array $values = []): array|false { return $this->run($sql, $values)->fetch(); }
    private function transaction(callable $operation): mixed
    {
        $this->db->exec('BEGIN IMMEDIATE');
        try { $result = $operation(); $this->db->exec('COMMIT'); return $result; }
        catch (Throwable $error) { $this->db->exec('ROLLBACK'); throw $error; }
    }

    public function accept(string $body, array $headers): array
    {
        // Whole-service cap also bounds remote verification calls for unsigned Ping/PayPal.
        $this->transaction(function (): void {
            $window = (int) floor(time() / 60);
            $this->run('DELETE FROM ingress WHERE window<?', [$window - 1]);
            $this->run('INSERT INTO ingress(window,requests) VALUES(?,1) ON CONFLICT(window) DO UPDATE SET requests=requests+1', [$window]);
            storeRequire((int) $this->one('SELECT requests FROM ingress WHERE window=?', [$window])['requests'] <= 60, 'INGRESS_RATE_LIMITED');
        });
        $authenticated = storeAuthenticate($this->config['provider'], $body, $headers, $this->config, $this->remote);
        $native = $authenticated['native'];
        if ($this->config['provider'] === 'fastspring') {
            $events = $native['events'] ?? [];
            storeRequire(is_array($events) && array_is_list($events) && count($events) >= 1 && count($events) <= 50, 'INVALID_EVENT_BATCH');
            return $this->transaction(function () use ($events, $headers): array {
                $allDuplicate = true;
                foreach ($events as $event) {
                    storeRequire(is_array($event) && is_string($event['id'] ?? null) && strlen($event['id']) >= 1 && strlen($event['id']) <= 190, 'EVENT_REFERENCE_REQUIRED');
                    $id = commandIdentity('fastspring', $this->config['seller_id'], $this->config['environment'], $event['id']);
                    $hash = hash('sha256', commandJson($event));
                    $previous = $this->one('SELECT body_hash FROM inbox WHERE id=?', [$id]);
                    if ($previous) { storeRequire($previous['body_hash'] === $hash, 'EVENT_CONFLICT'); continue; }
                    $allDuplicate = false;
                    $this->run("INSERT INTO inbox(id,body_hash,native,headers,state,received_at) VALUES(?,?,?,?,'WAITING',?)", [$id, $hash,
                        json_encode(['native' => ['events' => [$event]]], JSON_THROW_ON_ERROR), json_encode($headers, JSON_THROW_ON_ERROR), time()]);
                }
                return ['accepted' => true, 'duplicate' => $allDuplicate];
            });
        }
        $identifier = match ($this->config['provider']) {
            'stripe', 'paypal' => $native['id'] ?? null, 'paddle' => $native['event_id'] ?? null,
            'shopify' => storeHeader($headers, 'x-shopify-webhook-id'),
            'fastspring' => $native['events'][0]['id'] ?? null,
            default => hash('sha256', $body)
        };
        storeRequire(is_string($identifier) && $identifier !== '' && strlen($identifier) <= 190, 'EVENT_REFERENCE_REQUIRED');
        $id = commandIdentity($this->config['provider'], $this->config['seller_id'], $this->config['environment'], $identifier); $hash = hash('sha256', $body);
        return $this->transaction(function () use ($id, $hash, $authenticated, $headers): array {
            $previous = $this->one('SELECT body_hash FROM inbox WHERE id=?', [$id]);
            if ($previous) { storeRequire($previous['body_hash'] === $hash, 'EVENT_CONFLICT'); return ['accepted' => true, 'duplicate' => true]; }
            $this->run("INSERT INTO inbox(id,body_hash,native,headers,state,received_at) VALUES(?,?,?,?,'WAITING',?)",
                [$id, $hash, json_encode($authenticated, JSON_THROW_ON_ERROR), json_encode($headers, JSON_THROW_ON_ERROR), time()]);
            return ['accepted' => true, 'duplicate' => false];
        });
    }

    private function license(string $id): array
    {
        $origin = rtrim($this->fulfillmentConfig['base_url'], '/');
        $response = storeRequest($origin, 'GET', '/api/v1/licenses/' . rawurlencode($id), ['Accept: application/json', 'Authorization: Bearer ' . (string) getenv('KDP_API_KEY')]);
        storeRequire(($response['success'] ?? false) && ($response['license']['id'] ?? '') === $id && ($response['license']['environment'] ?? '') === $this->config['environment'], 'LICENSE_NOT_CONFIRMED');
        return $response['license'];
    }
    private function order(string $id): array|false
    {
        $hash = commandIdentity($this->fulfillmentConfig['source'], $this->fulfillmentConfig['seller_id'], $this->config['environment'], $id);
        $statement = $this->fulfillmentDb->prepare('SELECT * FROM orders WHERE order_key=?'); $statement->execute([$hash]); return $statement->fetch();
    }

    private function prepare(array $event, array $decision, int $position): ?array
    {
        $existing = $this->one('SELECT * FROM commands WHERE inbox_id=? AND position=?', [$event['id'], $position]);
        if ($existing) { return $existing; }
        $rawOrder = $decision['order']; $orderId = 'order_' . hash('sha256', $rawOrder);
        $order = $this->order($orderId); $current = null;
        storeRequire(!$order || (int) $order['held'] === 0, 'ORDER_HELD_RECONCILE_FIRST');
        if ($order && $order['license_id']) { $current = $this->license($order['license_id']); }
        $state = $decision['state'];
        if ($state === 'paid') {
            if ($this->one('SELECT receipt FROM payments WHERE order_id=? AND receipt=?', [$rawOrder, $decision['receipt']])) { return null; }
            if (!$order) {
                foreach ($decision['links'] as $reference) { storeRequire(!$this->one('SELECT state FROM unresolved_denials WHERE reference=?', [$reference]), 'EARLIER_ADVERSE_EVENT_REQUIRES_REVIEW'); }
                storeRequire(($decision['initial'] ?? false) === true, 'INITIAL_PURCHASE_NOT_RECORDED'); $action = 'issue';
            }
            else {
                storeRequire($current !== null && $decision['recurring'] === true && $order['offer_id'] === $decision['offer'] && $order['email'] === $decision['customer']['email'], 'PLAN_OR_CUSTOMER_CHANGE_REQUIRES_REVIEW');
                $action = ($current['status'] ?? '') === 'suspended' ? 'restore' : 'renew';
            }
        } else {
            storeRequire($current !== null, 'PURCHASE_MAPPING_REQUIRES_REVIEW');
            $action = match ($state) { 'failed', 'disputed' => 'suspend', 'refunded' => 'revoke', 'ended' => 'suspend', 'cancelled' => 'record_cancellation', 'recovered' => 'restore', default => throw new StoreRefusal('UNSUPPORTED_DECISION') };
            if (($action === 'suspend' && $current['status'] === 'suspended') || ($action === 'restore' && $current['status'] === 'active') || ($action === 'revoke' && $current['status'] === 'revoked')) { return null; }
        }
        $effect = ($action === 'issue' && $this->config['provider'] === 'stripe') ? $decision['initial_key'] : null;
        if (!is_string($effect) || strlen($effect) < 8 || !commandId($effect)) { $effect = 'effect_' . hash('sha256', $event['id'] . ':' . $position . ':' . $action); }
        $command = ['version' => 1, 'source' => $this->fulfillmentConfig['source'], 'seller_id' => $this->fulfillmentConfig['seller_id'],
            'environment' => $this->config['environment'], 'event_id' => 'delivery_' . $event['id'] . '_' . $position,
            'effect_id' => $effect, 'order_id' => $orderId, 'sequence' => (int) ($order['last_sequence'] ?? 0) + 1,
            'action' => $action, 'offer_id' => $order ? $order['offer_id'] : $decision['offer'],
            'customer' => $order ? ['email' => $order['email'], 'name' => 'Verified store buyer'] : $decision['customer'],
            'expected_status' => $current['status'] ?? null, 'expected_expires_at' => $current['expires_at'] ?? null];
        $body = commandJson($command); $id = hash('sha256', $event['id'] . ':' . $position);
        $this->run("INSERT INTO commands(id,inbox_id,position,body,order_id,receipt,state) VALUES(?,?,?,?,?,?,'WAITING')",
            [$id, $event['id'], $position, $body, $rawOrder, $decision['receipt']]);
        return $this->one('SELECT * FROM commands WHERE id=?', [$id]);
    }

    private function execute(array $job): string
    {
        $stamp = (string) time(); $signature = 't=' . $stamp . ',v1=' . hash_hmac('sha256', $stamp . '.' . $job['body'], (string) getenv('KDP_FULFILLMENT_SECRET'));
        $this->fulfillment->accept($job['body'], $signature);
        $this->fulfillment->work();
        $command = json_decode($job['body'], true, 64, JSON_THROW_ON_ERROR);
        $hash = commandIdentity($command['source'], $command['seller_id'], $command['environment'], $command['effect_id']);
        $query = $this->fulfillmentDb->prepare('SELECT state FROM effects WHERE effect_key=?'); $query->execute([$hash]);
        storeRequire($query->fetchColumn() === 'DONE', 'FULFILLMENT_RESULT_REQUIRES_REVIEW');
        $this->run("UPDATE commands SET state='DONE' WHERE id=?", [$job['id']]);
        return $command['action'];
    }

    public function work(): array
    {
        $path = $this->directory . '/worker.lock'; storeRequire(!is_link($path), 'INVALID_LOCK');
        $lock = fopen($path, 'c'); storeRequire($lock !== false && flock($lock, LOCK_EX | LOCK_NB), 'STORE_WORKER_BUSY');
        $processed = 0;
        try {
            $events = $this->run("SELECT * FROM inbox WHERE state='WAITING' ORDER BY received_at,id LIMIT 20")->fetchAll();
            foreach ($events as $event) {
                try {
                    $normalizer = new StoreNormalizer($this->config, $this->remote, fn(string $reference): ?string => ($this->one('SELECT order_id FROM links WHERE reference=?', [$reference]) ?: [])['order_id'] ?? null);
                    $decisions = $event['decisions'] === null ? $normalizer->normalize(json_decode($event['native'], true, 64, JSON_THROW_ON_ERROR), json_decode($event['headers'], true, 64, JSON_THROW_ON_ERROR)) : json_decode($event['decisions'], true, 64, JSON_THROW_ON_ERROR);
                    $this->run('UPDATE inbox SET decisions=? WHERE id=?', [json_encode($decisions, JSON_THROW_ON_ERROR), $event['id']]);
                    foreach ($decisions as $position => $decision) {
                        if (!empty($decision['unmapped_reference'])) {
                            $this->run('INSERT OR IGNORE INTO unresolved_denials(reference,state) VALUES(?,?)', [$decision['unmapped_reference'], $decision['state']]);
                            throw new StoreRefusal('ADVERSE_EVENT_PRECEDES_PURCHASE_RECONCILE_FIRST');
                        }
                        $job = $this->prepare($event, $decision, $position * 2);
                        $action = $job ? $this->execute($job) : null;
                        // A recovered paid period needs both restore and one additive renewal.
                        if ($decision['state'] === 'paid' && $action === 'restore') {
                            $second = $this->prepare($event, $decision, $position * 2 + 1);
                            storeRequire($second !== null, 'RENEWAL_COMMAND_REQUIRED'); $this->execute($second);
                        }
                        $this->transaction(function () use ($decision): void {
                            if ($decision['state'] === 'paid') { $this->run('INSERT OR IGNORE INTO payments(order_id,receipt) VALUES(?,?)', [$decision['order'], $decision['receipt']]); }
                            foreach ($decision['links'] as $reference) {
                                if ($reference === '') { continue; }
                                $previous = $this->one('SELECT order_id FROM links WHERE reference=?', [$reference]);
                                storeRequire(!$previous || $previous['order_id'] === $decision['order'], 'PURCHASE_REFERENCE_CONFLICT');
                                $this->run('INSERT OR IGNORE INTO links(reference,order_id) VALUES(?,?)', [$reference, $decision['order']]);
                            }
                        });
                    }
                    $this->run("UPDATE inbox SET state='DONE' WHERE id=?", [$event['id']]);
                } catch (Throwable $error) {
                    $code = $error instanceof StoreRefusal && preg_match('/\A[A-Z_]{1,80}\z/D', $error->getMessage()) ? $error->getMessage() : 'RECONCILE_PROVIDER_AND_FULFILLMENT';
                    $this->run("UPDATE inbox SET state='REVIEW',error_code=? WHERE id=?", [$code, $event['id']]);
                }
                $processed++;
            }
        } finally { flock($lock, LOCK_UN); fclose($lock); }
        return ['processed' => $processed, 'states' => $this->inspect()];
    }
    public function inspect(): object
    {
        $states = []; foreach ($this->run('SELECT state,COUNT(*) AS count FROM inbox GROUP BY state')->fetchAll() as $row) { $states[$row['state']] = (int) $row['count']; }
        return (object) $states;
    }
}

if (defined('KDP_STORE_LIBRARY_ONLY') && KDP_STORE_LIBRARY_ONLY === true) { return; }
try {
    $service = new StoreService();
    if (PHP_SAPI === 'cli') {
        if (($argv[1] ?? '') === 'work' && $argc === 2) { $result = $service->work(); }
        elseif (($argv[1] ?? '') === 'inspect' && $argc === 2) { $result = ['states' => $service->inspect()]; }
        else { throw new StoreRefusal('USE_WORK_OR_INSPECT'); }
        echo json_encode($result, JSON_THROW_ON_ERROR) . PHP_EOL; exit;
    }
    if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST' || ($_SERVER['REQUEST_URI'] ?? '') !== '/provider-event') { referenceReply(404, ['accepted' => false]); }
    if (!ctype_digit((string) ($_SERVER['CONTENT_LENGTH'] ?? '')) || isset($_SERVER['HTTP_TRANSFER_ENCODING'])) { referenceReply(400, ['accepted' => false]); }
    if ((int) $_SERVER['CONTENT_LENGTH'] > 262144) { referenceReply(413, ['accepted' => false]); }
    $headers = []; foreach (getallheaders() as $name => $value) { $headers[strtolower($name)] = $value; }
    $allowed = ['stripe-signature', 'paddle-signature', 'x-signature', 'x-wc-webhook-signature', 'x-wc-webhook-topic', 'x-shopify-hmac-sha256', 'x-shopify-topic', 'x-shopify-shop-domain', 'x-shopify-webhook-id', 'x-fs-signature', 'paypal-auth-algo', 'paypal-cert-url', 'paypal-transmission-id', 'paypal-transmission-sig', 'paypal-transmission-time'];
    $headers = array_intersect_key($headers, array_flip($allowed));
    $raw = file_get_contents('php://input', false, null, 0, 262145);
    storeRequire(is_string($raw) && strlen($raw) === (int) $_SERVER['CONTENT_LENGTH'], 'INVALID_BODY');
    referenceReply(202, $service->accept($raw, $headers));
} catch (Throwable $error) {
    if (PHP_SAPI === 'cli') { echo '{"completed":false,"error":"REVIEW_PRIVATE_STORE_STATE"}' . PHP_EOL; exit(1); }
    $status = $error instanceof StoreRefusal ? match ($error->getMessage()) {
        'INGRESS_RATE_LIMITED' => 429, 'EVENT_CONFLICT' => 409, 'INVALID_BODY' => 400, default => 401
    } : 503;
    referenceReply($status, ['accepted' => false]);
}
```

## Expected result

An authenticated request is durably saved before a 202 receipt. work and inspect print only aggregate states; unknown effects are held instead of blindly repeated. Private SQLite state and the fulfillment ledger must survive restarts. [Provider guides](/docs/v1/guides/store-service#choose-your-provider) define accepted payment evidence and unsupported cases. Native events are not public licensing commands.


## Prerequisites

Complete the [private service installation](/docs/v1/guides/store-service). Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product)/key. A source fragment alone is not the application.


## 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](/docs/v1/start/glossary#delivery) evidence. Do not post raw events, keys or SQLite files publicly.


## Next steps

Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling [Live](/docs/v1/start/glossary#live).


## Related

- [Generic store trust boundary](/docs/v1/guides/any-store)
- [API idempotency](/docs/v1/api/idempotency)
- [Private service setup](/docs/v1/guides/store-service)
- [Manual key delivery](/docs/v1/guides/manual-issuance)
