Translate a verified purchase into a durable backend command, then call the existing licensing API once for that authorized effect.
What you will learn
- Separate payment-provider verification from licensing fulfillment.
- Configure approved offer mappings without accepting a plan from a browser.
- Run a complete Node.js, Python, PHP or C# reference receiver and worker.
- Preserve a paid customer's access and stop safely after an uncertain result.
Prerequisites
- Complete the Sandbox quickstart.
- Read create license and idempotency, including the known replay limitations.
- Create a product and plan; keep their ID or slug mapping privately.
- Use a backend developer key with licenses:read and licenses:write for that product and environment.
- Have a trusted store backend that can verify the provider's real purchase, refund and subscription state.
- Keep private state, backups and exports outside every web root; start in Sandbox.
Separate the three contracts
| Boundary | Who authenticates it | What success proves |
|---|---|---|
| Store/provider → your adapter | Your adapter uses that provider's actual signature/API verification and confirms merchant, environment, recipient, purchased offer and current state | A particular business effect is authorized; not that licensing completed |
| Your adapter → reference receiver | Separate private command HMAC, approved mapping and durable effect/sequence checks | HTTP 202 means durable receipt, not payment confirmation, license issuance or an email |
| Reference worker → KDPika | Scoped backend developer key, existing API v1, explicit accepted response | The corresponding license action completed; a full new key is encrypted locally for delivery |
KDPika does not process payments from your software buyers. The platform's own billing integration is not your store adapter. A browser return URL, success parameter, unsigned JSON field or customer-selected plan is never payment evidence. The private format below is not a new public KDPika endpoint or a native event format from a provider.
License outbound notifications are a fourth, separate flow. Their X-LMP-Signature must not be treated as a payment-provider signature or reused as this command secret.
Choose the complete reference implementation
Download the complete source archive. It contains all four servers, project/dependency files, the private protocol, example mapping, example command and tested command sender. The source manifest records the archive and member SHA-256 values. No state database, credentials, buyer files, binaries or exported keys are included.
All four implementations run on POSIX macOS/Linux-oriented backends. They are not native Windows ACL/lock examples. Your selected hosting must support the chosen runtime and private persistent SQLite filesystem. PHP shared hosting can use the PHP implementation; the other runtimes are not requirements for KDPika's hosting.
Configure an approved offer mapping
- Copy config.example.json to an absolute private file outside your web root.
- Replace source and seller_id with stable nonpersonal IDs identifying your store and merchant account.
- Keep environment as sandbox for this test.
- Set base_url to the HTTPS platform origin, without /api/v1, a path, query or fragment.
- Replace product_id with your saved product reference.
- Replace each offer key with an independently verified provider price/item reference and its plan_id.
- Choose renewal_days deliberately for that offer's renewal policy.
{
"source": "your-store",
"seller_id": "YOUR_STORE_ACCOUNT",
"environment": "sandbox",
"base_url": "https://licensing.kdpika.com",
"product_id": "YOUR_PRODUCT_ID",
"offers": {
"YOUR_VERIFIED_PRICE_ID": {
"plan_id": "YOUR_PLAN_ID",
"renewal_days": 30
}
}
}
| Mapping field | Trusted source | Reject this alternative |
|---|---|---|
| source / seller_id | Pinned backend account configuration | Merchant supplied by an unauthenticated caller |
| environment | Explicit Sandbox or Live adapter configuration and provider mode | A URL parameter used to bypass a mismatch |
| product_id / plan_id | Approved product mapping you saved from your workspace | Arbitrary browser-selected KDPika plan |
| offer ID | Verified provider object and purchased line | Display name or unverified callback metadata alone |
| recipient | Confirmed order/buyer record and your delivery policy | An arbitrary replacement email on a retry |
| order ID | Stable purchase line, including subscription line where needed | A fresh random ID each time a webhook arrives |
| effect ID | One durable business action, such as one paid invoice/period | A new ID for every delivery or overlapping event type |
| sequence | Your adapter's durable authorized command order | Provider delivery time or incrementing every arrival |
Creation uses the selected product plan's stored duration. Renewal adds the mapped integer days; 30 days is not automatically a calendar month, and 365 days is not every calendar year. The existing API has no exact provider-period-end setter or in-place plan-change endpoint. Choose an explicit supported entitlement policy, or use a reviewed replacement-license workflow in recipes. Do not advertise exact period alignment that your mapping cannot provide.
Keep secrets and state private
| Environment variable | Required value |
|---|---|
| KDP_FULFILLMENT_CONFIG | Absolute path to the private mapping JSON |
| KDP_FULFILLMENT_STATE | Absolute owner-only persistent directory outside public_html or other web roots |
| KDP_FULFILLMENT_SECRET | At least 32 unpredictable characters supplied by your secret store; separate from all provider and outbound notification secrets |
| KDP_FULFILLMENT_OUTBOX_KEY | Base64 of 32 random bytes; back up separately from the encrypted ledger |
| KDP_API_KEY | Product/environment-scoped backend key; never a client App ID or installation token |
| KDP_FULFILLMENT_PORT | Optional Node/Python/C# loopback port; default 8112 |
| KDP_CONFIRM_LIVE | Explicit 1 in addition to a Live mapping/key; never set for these tests |
| KDP_ALLOW_LOOPBACK | Test-only 1 permits literal HTTP 127.0.0.1 API/receiver URLs in Sandbox; never a public TLS exception |
Use your hosting's private environment/secret configuration rather than committing values or pasting them into public support logs. Keep state directory mode 0700 and private files 0600. The implementations refuse broad state permissions and bind the database to the mapping and encryption-key fingerprints. API key rotation does not erase order history. Changing a merchant, product, offer mapping or encryption key needs reviewed reconciliation/migration, not an empty state directory.
Create an authorized command
- Verify a provider event using that provider's documented authentication.
- Fetch or reconcile the actual current payment/order/subscription where the provider requires it.
- Confirm account ownership, environment, purchased item, payment outcome and recipient.
- Assign or recover the durable effect ID and command sequence in your adapter's own ledger.
- Save a complete command to an absolute private JSON file.
{
"version": 1,
"source": "your-store",
"seller_id": "YOUR_STORE_ACCOUNT",
"environment": "sandbox",
"event_id": "YOUR_PROVIDER_EVENT_ID",
"effect_id": "YOUR_STABLE_PAID_ORDER_LINE_EFFECT",
"order_id": "YOUR_STABLE_ORDER_LINE_ID",
"sequence": 1,
"action": "issue",
"offer_id": "YOUR_VERIFIED_PRICE_ID",
"customer": {
"email": "buyer@example.invalid",
"name": "Demo Buyer"
},
"expected_status": null,
"expected_expires_at": null
}
All shown fields are required; unknown fields are rejected. IDs use 1–190 ASCII letters, digits, underscore, period, colon or hyphen. Do not include personal details or keys in IDs. issue requires sequence 1 and null expected fields. Other actions require the recorded expected license status and exact expires_at value, including null if appropriate.
Different provider event types can describe the same purchase. For example, a checkout completion and an invoice payment must not each become a separate issuance. Choose one business effect and persist its identity. A second delivery reuses its effect and sequence. Out-of-order commands wait for the missing sequence; assigning a fresh sequence to stale state is not reconciliation.
Send to the private receiver
The supplied sender signs the exact file bytes with X-Fulfillment-Signature. Its message is ASCII UNIX timestamp, one period and the raw body, using HMAC-SHA256. The receiver enforces a 300-second timestamp window, a 262144-byte body limit and exact merchant/environment/mapping checks. These are reference policies, not changes to provider or KDPika contracts.
- Start your selected reference's loopback receiver as documented on its language page.
- Configure KDP_FULFILLMENT_URL privately as the receiver's exact /store-event URL.
- Run the command sender only after the authorization steps above.
php send-command.php /private/verified-command.json
Expected receipt: {"accepted":true,"duplicate":false}. An unchanged replay returns duplicate:true. A conflict/refusal returns a nonzero sender exit and a plain instruction to inspect the private receiver; it never retries automatically. HTTP 202 is not a statement that a license or email already exists.
PHP/cURL is required only for this supplied sender CLI; the four HTTP receivers accept the same authenticated protocol. A production adapter can implement the exact HMAC privately in its own language. It must preserve durable receipt uncertainty and stable event/effect identities; it must not forward unverified buyer JSON.
Process and deliver
- Run work using your selected implementation's CLI or private cron.
- Inspect its safe processed/states JSON.
- For DONE issuance, authenticate the intended recipient before exporting the saved key.
- Run the selected implementation's export command with the stable order ID and a new absolute private output path.
- Deliver through your private authenticated delivery workflow.
- Retain the durable business-effect record so a repeated event cannot reissue.
Each work run processes at most 25 ready effects and waits 1.1 seconds after an attempted effect. This is a local pacing safeguard, not an exemption from the platform's shared rate limits. Separate orders can still progress when one order is held.
| Action | Behavior | Important boundary |
|---|---|---|
| issue | Create once with stable effect-derived Idempotency-Key; encrypt the returned full key using AES-256-GCM | A lost successful response cannot be rolled back or safely reissued automatically |
| renew | Verify current scoped license, then add the configured integer days once | Unknown result is held; no blind additive retry |
| suspend | Apply the eligible server suspension transition | Active installation tokens are revoked |
| restore | Apply eligible restoration | Does not revive revoked installation tokens or a terminally revoked license |
| revoke | Apply terminal revocation after your verified policy decision | A later dispute/refund reversal does not automatically restore it |
| record_cancellation | Save the verified intent without removing paid access | Does not cancel/refund provider billing or schedule future work |
The encrypted outbox and explicit export are not an email service. No full key is printed by work/inspect/export. A successful export means a new owner-only key file exists, not that the buyer received it. Reuse the saved key for authenticated delivery; do not create another license to resend. Review manual key delivery for the corresponding private workflow.
Reconcile unknown results
| State | Meaning | What to do |
|---|---|---|
| WAITING | Durable receipt awaiting its per-order sequence | Reconcile the missing authorized command; never skip a gap blindly |
| IN_FLIGHT | Intent saved before the remote call; interruption may have hidden its result | Hold the order and inspect provider/dashboard evidence before another write |
| REVIEW | API/transport/response/state did not meet the accepted result checks | Do not interpret it as proof no license exists; investigate first |
| DONE | Explicit accepted result saved with the local completion state | Deliver or inspect; unchanged event replay is harmless |
The current API has known create-replay expiration/concurrency limitations and no public license-list/search endpoint to automatically recover an unknown new ID. The local ledger is durable beyond the API's 24-hour replay retention, but cannot guarantee cross-system exactly-once behavior. Keep backups; never delete a held record or invent another order to get a green result.
Before a non-issue mutation the worker compares a scoped GET with the authorized expected status/expiration. That read and later write are not atomic compare-and-swap. Keep one reviewed writer for that order; manual dashboard changes or another integration can race it. Stop and reconcile such changes rather than claiming automatic consistency.
Node's built-in modules do not expose flock. Its atomic worker.lockdir remains after a killed worker. Confirm the worker is stopped, reconcile held IN_FLIGHT effects, and remove only that reviewed empty lock directory. Never run automatic stale-lock deletion or mix language workers in one state directory. The other three references use POSIX flock.
Expected result
The exact four sources pass actual local Sandbox receiver/worker tests: receipt before issuance, duplicate/conflicting events, signature/mode/mapping refusal, real create/activate/renew/suspend/restore/revoke, durable restart, encrypted export and a gateway that hides a genuinely successful 201. The worker holds the unknown result and sends no second create. This is local application evidence, not a real external provider purchase or SMTP delivery test.
The C# reference builds with zero warnings on .NET 10; its NuGet lock records exact direct/transitive versions. Python's exact requirements are executed in an isolated environment. Package-advisory checks cover known database entries at the recorded check time, not a certification or absence of future vulnerabilities. Recheck dependencies and supported runtimes before go-live.
Common mistakes
- Accepting the private example command directly from an untrusted buyer: only your authenticated, verified adapter may authorize it.
- Treating a signed provider event as sufficient without confirming merchant, mode and current purchase outcome.
- Using a provider delivery ID as the sole business-effect dedupe key when several event types describe one paid purchase.
- Cancelling an already-paid license immediately when a subscription only stops renewing.
- Deleting the ledger to retry an unknown create, or silently changing offer mappings on an existing state directory.
Security notes
- Use TLS and a reviewed ingress with body limits, timeouts and rate controls before any external deployment. Loopback development servers are not production internet servers.
- PHP's SAPI may buffer request bodies before the script starts; configure the web server's 256 KiB limit as well as the application check. Ask the shared host for the correct per-site setting.
- Keep full keys, command payloads and recipient email out of web logs, Git, screenshots and analytics. The local ledger contains buyer email/name and needs your retention/access policy.
- Back up encrypted state and its key separately. Test recovery; encryption does not replace filesystem isolation or protect a compromised backend that holds both.
- Give the backend key only the required product/environment and read/write scopes. Never put it in desktop/mobile/browser application code.
- Do not add card collection, provider secret configuration or payment callbacks to KDPika to use these examples.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Command not confirmed | Receipt was refused or transport result is unknown | Inspect the existing inbox/effect before any resend; preserve IDs and exact meaning |
| 401 | Wrong/duplicate/stale signature or modified body | Check the separate command secret and clock; sign exact bytes after serialization |
| 409 | Merchant/environment/mapping or event/effect/order conflict | Correct trusted configuration; reconcile existing records instead of bypassing them |
| 413 | Body exceeds the local reference bound | Keep commands small; do not send raw provider histories or attachments |
| WAITING never executes | Earlier sequence missing or order held | Reconcile the actual authorized history and held result |
| Work exits with safe configuration error | State permissions, key/config drift or busy lock | Inspect private configuration and worker ownership; never publish secrets to debug |
| REVIEW after successful provider payment | Licensing result was refused, mismatched or unknown | Keep payment and licensing evidence separate; use request_id/dashboard support to reconcile |
| Buyer has no email | No email-delivery workflow was implemented | Deliver the saved key through your private authenticated workflow; do not reissue |
Next steps
Follow your chosen reference page, test Sandbox issuance and refusal cases, then review licensing recipes before defining renewal and refund policies.