These complete PHP helpers belong to the store application. Native provider proof is deliberately separate from outbound X-LMP-Signature and the private fulfillment-command signature.
What you will learn
Read and run the exact Store service: native proof and HTTP client with the complete shared application, without exposing private credentials or claiming an unverified payment.
Native authentication
<?php
declare(strict_types=1);
// Native provider authentication for the store reference service, not LMP webhooks.
// PHP 8.2+. Required secrets come from the private process environment.
// Included by service.php. Returns verified body identity or throws; never logs raw input.
final class StoreRefusal extends RuntimeException {}
function storeRequire(bool $condition, string $code): void
{
if (!$condition) {
throw new StoreRefusal($code);
}
}
function storeHeader(array $headers, string $name): string
{
$value = $headers[strtolower($name)] ?? '';
storeRequire(is_string($value) && strlen($value) <= 4096, 'INVALID_HEADER');
return $value;
}
function storeTimestampHmac(string $raw, string $header, string $secret, string $separator, string $timeKey, string $hashKey, int $tolerance, int $clock): void
{
$parts = [];
foreach (explode($separator, $header) as $entry) {
$pair = explode('=', trim($entry), 2);
storeRequire(count($pair) === 2 && in_array($pair[0], [$timeKey, $hashKey], true), 'INVALID_SIGNATURE');
$parts[$pair[0]][] = $pair[1];
}
storeRequire(count($parts[$timeKey] ?? []) === 1 && count($parts[$hashKey] ?? []) >= 1 && count($parts[$hashKey]) <= 8, 'INVALID_SIGNATURE');
$stamp = $parts[$timeKey][0];
storeRequire(preg_match('/\A[1-9][0-9]{0,10}\z/D', $stamp) === 1 && abs($clock - (int) $stamp) <= $tolerance, 'INVALID_SIGNATURE');
$expected = hash_hmac('sha256', $stamp . ($timeKey === 'ts' ? ':' : '.') . $raw, $secret);
$valid = false;
foreach ($parts[$hashKey] as $candidate) {
storeRequire(preg_match('/\A[0-9a-f]{64}\z/D', $candidate) === 1, 'INVALID_SIGNATURE');
$valid = hash_equals($expected, $candidate) || $valid;
}
storeRequire($valid, 'INVALID_SIGNATURE');
}
function storeAuthenticate(string $provider, string $raw, array $headers, array $config, callable $remote, ?int $clock = null): array
{
storeRequire(strlen($raw) <= 262144, 'BODY_TOO_LARGE');
$clock ??= time();
$secret = (string) getenv('KDP_STORE_WEBHOOK_SECRET');
if (in_array($provider, ['stripe', 'paddle', 'lemon-squeezy', 'woocommerce', 'shopify', 'fastspring'], true)) {
storeRequire(strlen($secret) >= 16, 'WEBHOOK_SECRET_REQUIRED');
}
if ($provider === 'stripe') {
storeTimestampHmac($raw, storeHeader($headers, 'stripe-signature'), $secret, ',', 't', 'v1', 300, $clock);
} elseif ($provider === 'paddle') {
storeTimestampHmac($raw, storeHeader($headers, 'paddle-signature'), $secret, ';', 'ts', 'h1', 5, $clock);
} elseif ($provider === 'lemon-squeezy') {
$signature = storeHeader($headers, 'x-signature');
storeRequire(preg_match('/\A[0-9a-f]{64}\z/D', $signature) === 1 && hash_equals(hash_hmac('sha256', $raw, $secret), $signature), 'INVALID_SIGNATURE');
} elseif (in_array($provider, ['woocommerce', 'shopify', 'fastspring'], true)) {
$name = ['woocommerce' => 'x-wc-webhook-signature', 'shopify' => 'x-shopify-hmac-sha256', 'fastspring' => 'x-fs-signature'][$provider];
$signature = storeHeader($headers, $name);
storeRequire(hash_equals(base64_encode(hash_hmac('sha256', $raw, $secret, true)), $signature), 'INVALID_SIGNATURE');
if ($provider === 'shopify') {
storeRequire(storeHeader($headers, 'x-shopify-shop-domain') === ($config['shop_domain'] ?? null), 'WRONG_SHOP');
storeRequire(in_array(storeHeader($headers, 'x-shopify-topic'), ['orders/paid', 'refunds/create', 'orders/cancelled'], true), 'UNSUPPORTED_TOPIC');
}
} elseif ($provider === 'gumroad') {
// Gumroad Ping has no documented native HMAC. A sale ID is only a lookup hint.
parse_str($raw, $hint);
$id = $hint['sale_id'] ?? '';
storeRequire(is_string($id) && strlen($id) >= 4 && strlen($id) <= 190, 'SALE_REFERENCE_REQUIRED');
$verified = $remote('GET', '/v2/sales/' . rawurlencode($id), null);
storeRequire(($verified['success'] ?? null) === true && ($verified['sale']['id'] ?? null) === $id, 'SALE_NOT_VERIFIED');
storeRequire(($verified['sale']['seller_id'] ?? null) === $config['seller_id'], 'WRONG_SELLER');
return ['native' => $hint, 'verified_sale' => $verified['sale']];
} elseif ($provider === 'paypal') {
$event = json_decode($raw, true, 64, JSON_THROW_ON_ERROR);
$request = ['webhook_id' => $config['webhook_id'], 'webhook_event' => $event];
foreach (['auth_algo', 'cert_url', 'transmission_id', 'transmission_sig', 'transmission_time'] as $field) {
$request[$field] = storeHeader($headers, 'paypal-' . str_replace('_', '-', $field));
storeRequire($request[$field] !== '', 'INVALID_PAYPAL_HEADERS');
}
// No fetching of an untrusted certificate URL by this application.
$result = $remote('POST', '/v1/notifications/verify-webhook-signature', $request);
storeRequire(($result['verification_status'] ?? null) === 'SUCCESS', 'INVALID_SIGNATURE');
} else {
throw new StoreRefusal('UNSUPPORTED_PROVIDER');
}
$event = json_decode($raw, true, 64, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING);
storeRequire(is_array($event) && !array_is_list($event), 'INVALID_EVENT');
return ['native' => $event];
}
Fixed-origin provider client
<?php
declare(strict_types=1);
// Fixed-origin provider HTTP client for service.php; PHP 8.2+, cURL, OpenSSL.
// API credentials are private environment values. Never prints request URLs/bodies.
function storeRequest(string $origin, string $method, string $path, array $headers, array|string|null $body = null, ?string $basic = null): array
{
storeRequire(str_starts_with($path, '/') && !str_starts_with($path, '//') && !preg_match('/[\r\n#]/', $path), 'INVALID_API_PATH');
$raw = '';
$handle = curl_init($origin . $path);
$options = [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_PROTOCOLS => str_starts_with($origin, 'https://') ? CURLPROTO_HTTPS : CURLPROTO_HTTP,
CURLOPT_WRITEFUNCTION => static function ($unused, string $chunk) use (&$raw): int {
if (strlen($raw) + strlen($chunk) > 1048576) { return 0; }
$raw .= $chunk; return strlen($chunk);
}];
if ($basic !== null) { $options[CURLOPT_USERPWD] = $basic; $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; }
if ($body !== null) { $options[CURLOPT_POSTFIELDS] = is_string($body) ? $body : json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); }
curl_setopt_array($handle, $options);
$sent = curl_exec($handle); $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
storeRequire($sent !== false && $status >= 200 && $status < 300, 'PROVIDER_API_UNCONFIRMED');
$value = json_decode($raw, true, 64, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING);
storeRequire(is_array($value) && !array_is_list($value), 'INVALID_PROVIDER_RESPONSE');
return $value;
}
function storeApi(array $config): Closure
{
$provider = $config['provider']; $sandbox = $config['environment'] === 'sandbox';
$origins = ['stripe' => 'https://api.stripe.com',
'paddle' => $sandbox ? 'https://sandbox-api.paddle.com' : 'https://api.paddle.com',
'lemon-squeezy' => 'https://api.lemonsqueezy.com', 'gumroad' => 'https://api.gumroad.com',
'paypal' => $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com',
'fastspring' => 'https://api.fastspring.com'];
if ($provider === 'shopify') {
storeRequire(preg_match('/\A[a-z0-9][a-z0-9-]*\.myshopify\.com\z/D', $config['shop_domain']) === 1, 'INVALID_SHOP');
$origin = 'https://' . $config['shop_domain'];
} elseif ($provider === 'woocommerce') {
$origin = $config['store_url'] ?? '';
$url = parse_url($origin);
storeRequire($url && ($url['scheme'] ?? '') === 'https' && !empty($url['host']) && !isset($url['user']) && !isset($url['pass']) && empty($url['path']) && !isset($url['query']) && !isset($url['fragment']), 'INVALID_STORE_ORIGIN');
} else { $origin = $origins[$provider] ?? throw new StoreRefusal('UNSUPPORTED_PROVIDER'); }
$test = (string) getenv('KDP_STORE_TEST_API');
if ($test !== '') {
$url = parse_url($test);
storeRequire($sandbox && getenv('KDP_STORE_ALLOW_LOOPBACK') === '1' && ($url['scheme'] ?? '') === 'http' && ($url['host'] ?? '') === '127.0.0.1' && empty($url['path']) && !isset($url['user']), 'INVALID_TEST_API');
$origin = $test;
}
$key = (string) getenv('KDP_STORE_API_KEY');
if ($provider === 'paypal' && $key === '') {
$client = (string) getenv('KDP_STORE_CLIENT_ID'); $clientSecret = (string) getenv('KDP_STORE_CLIENT_SECRET');
storeRequire(strlen($client) >= 8 && strlen($clientSecret) >= 8, 'PRIVATE_PAYPAL_CLIENT_REQUIRED');
$token = storeRequest($origin, 'POST', '/v1/oauth2/token', ['Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'], 'grant_type=client_credentials', $client . ':' . $clientSecret);
$key = (string) ($token['access_token'] ?? '');
storeRequire(($token['token_type'] ?? '') === 'Bearer', 'PAYPAL_TOKEN_NOT_CONFIRMED');
}
$basic = null; $headers = ['Accept: application/json', 'Content-Type: application/json'];
if ($provider === 'stripe') {
storeRequire(str_starts_with($key, $sandbox ? 'sk_test_' : 'sk_live_'), 'PROVIDER_KEY_MODE_MISMATCH');
$headers[] = 'Stripe-Version: ' . $config['api_version'];
}
if ($provider === 'shopify') { $headers[] = 'X-Shopify-Access-Token: ' . $key; }
elseif (in_array($provider, ['fastspring', 'woocommerce'], true)) { $basic = $key . ':' . (string) getenv('KDP_STORE_API_SECRET'); }
elseif ($provider === 'gumroad') { /* Native API access_token parameter is appended privately below. */ }
else { $headers[] = 'Authorization: Bearer ' . $key; }
if ($provider === 'lemon-squeezy') { $headers = ['Accept: application/vnd.api+json', 'Content-Type: application/vnd.api+json', 'Authorization: Bearer ' . $key]; }
storeRequire(strlen($key) >= 8, 'PRIVATE_PROVIDER_KEY_REQUIRED');
return static function (string $method, string $path, ?array $body = null) use ($origin, $provider, $key, $basic, $headers): array {
if ($provider === 'gumroad') { $path .= (str_contains($path, '?') ? '&' : '?') . 'access_token=' . rawurlencode($key); }
return storeRequest($origin, $method, $path, $headers, $body, $basic);
};
}
Expected result
Missing/invalid native proof is refused; provider reads use pinned origins, TLS verification, no redirects, bounded bodies and deadlines. Gumroad uses a merchant-authenticated sale lookup rather than an invented Ping signature. PayPal verifies through its own API rather than downloading an arbitrary certificate URL. These are reference controls, not an audit/certification or a guarantee against all attacks.
Prerequisites
Complete the private service installation. Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated Sandbox 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 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.