KDPikaLicensing
API v1
Show all languages
Browse documentation

Choose a page in the navigation drawer.

KDPika Licensing · API v1 Beginner

Python credits: one export, one charge

Add an online credit charge and durable retry key before doing a real CSV job.

On this page
Updated 2026-10-26 UTCDownload Markdown

The simple rule

Confirm one credit charge, then do one job. Checking a feature does not spend a credit. A credit is a usage unit, not a euro payment or subscription invoice.

This example writes a real CSV and records its job in a private SQLite file. It uses the real SDK and a real metered Sandbox license. It does not mock billing, collect bank cards or sell credit packs.

1. Prepare a credit license

  1. Your KDPika platform plan must include usage credits. Check Billing & usage and your current plan capabilities.
  2. Create a separate tutorial software plan with Uses credits enabled. Start without floating seats to learn one model at a time.
  3. Issue its Sandbox license to an email you control.
  4. Open that license → Credits and grant a small whole-number balance, such as 3 units. The application consumes units; it does not carry a developer management key for granting them.

The operation itself is credit-gated in this example; it does not additionally require csv_export. If your real product combines feature flags and credits, design and test both the first authorization and recovery of already-paid jobs. Do not add a feature check after the final successful charge and accidentally reject the paid operation because the balance reached zero.

Dashboard exampleExample data · not your account
Plans & features → New plan

Define access and entitlements

Sandbox
1Plan name
Export credits
Max devices
2
Apps running at once
Leave blank
Learn credit charging without mixing in shared seats.
2Usage credits (pay per use)Selected
Feature keys
Leave blank
The credit job is authorized by its signed consume receipt.
3Create plan

New metered licenses start with zero credits. Issue a new Sandbox license for this plan, then add units on that license.

Dashboard exampleExample data · not your account
Licenses → Manage → Usage credits

Manage usage credits

Sandbox
Available credits0 → 3Example: grant 3 units to a newly issued metered license.
1Action
Add credits
2Credits
3
Note (optional)
Initial tutorial allowance
3Save credit transaction

Usage units, not money. This does not charge a card or refund a payment.

2. Run one job

Use the extracted Python projects, the same SDK installation and App ID/Sandbox environment setup from the desktop or web guide.

bashbash
python credit_export.py --job export-order-001 --text "My paid export"

On first activation, enter the credit license key privately and its matching email. After a verified successful consume, the app prints the saved CSV path. Open Credits in KDPika: one unit was consumed.

3. Repeat the identical command

Run that exact command again, with the same job name, message, license and private state folder. The completed job returns its saved output without consuming another unit.

A genuinely new export uses a new logical job name. A retry of an uncertain export uses its original name. Do not generate a fresh UUID every time the retry button is clicked.

SituationSafe response
Signed charge succeedsDo that one job, even if the resulting balance is zero
Not enough creditsDo not do the job. Ask the publisher to add units, then retry the same job
Connection drops before a replyThe outcome is uncertain; retain the pending job and retry its original key
Response is replayedIt identifies the original charge; do not perform a second independent job
Job name reused with different data/licenseRefuse and ask the user to resume the original job or create a genuinely new one

4. Understand the code

client.consume(1, idempotency_key=...) verifies the signed charge response. The server prevents a repeated request key from charging twice. SQLite retains the job before that request, and a deterministic output path/atomic replacement makes this CSV work repeatable after a crash.

The job journal is local application data, not proof of a valid license. Every pending run checks the online license and requires a verified consume receipt. A zero balance is allowed only for recovery of a receipt that already paid for that same operation; it is not permission for another job.

credit_export.pypython
"""One credit per CSV job. SQLite retains each request key across retries/restarts."""
import argparse
import csv
import getpass
import hashlib
import os
import re
import sqlite3
import tempfile
from pathlib import Path

from common import AccessDenied, application_folder, make_client
from kdpika_licensing import CreditOperationError, InsufficientCreditsError


def run_job(client, job_id: str, text: str, folder: Path) -> Path:
    if not re.fullmatch(r"[A-Za-z0-9_-]{8,80}", job_id) or not text or len(text) > 1000:
        raise ValueError("Use an 8–80 character job name and a message of 1–1000 characters.")
    # A zero balance may be the result of an already-charged pending job. It must
    # still be recoverable with that job's original key, never a new charge key.
    result = client.check(force_online=True)
    if not result.signature_valid or (not result.valid and result.error_code != "INSUFFICIENT_CREDITS"):
        raise AccessDenied("Reconnect and check this license before exporting.")
    if not result.payload.get("metered"):
        raise AccessDenied("Choose a usage-credit license for this example.")
    folder.mkdir(parents=True, exist_ok=True, mode=0o700)
    name = hashlib.sha256(job_id.encode()).hexdigest()
    target = folder / (name + ".csv")
    db = sqlite3.connect(folder / "jobs.sqlite3", timeout=30)
    db.execute("CREATE TABLE IF NOT EXISTS jobs (id TEXT PRIMARY KEY, license_id TEXT NOT NULL, text TEXT NOT NULL, state TEXT NOT NULL)")
    temporary = None
    try:
        db.execute("BEGIN IMMEDIATE")  # One local worker owns this job until its result is saved.
        row = db.execute("SELECT license_id, text, state FROM jobs WHERE id=?", (job_id,)).fetchone()
        if row and (row[0] != result.license_id or row[1] != text):
            raise AccessDenied("Keep this job's original license and message when retrying.")
        if row and row[2] == "DONE" and target.is_file():
            db.commit()
            return target  # Reopening a completed output does not charge or execute a new job.
        if row is None:
            db.execute("INSERT INTO jobs VALUES (?, ?, ?, 'PENDING')", (job_id, result.license_id, text))
            db.commit()  # The key must exist durably BEFORE the first network request.
            db.execute("BEGIN IMMEDIATE")
            # Another process may have finished after the durable insert.
            row = db.execute("SELECT state FROM jobs WHERE id=?", (job_id,)).fetchone()
            if row and row[0] == "DONE" and target.is_file():
                db.commit()
                return target
        client.consume(1, idempotency_key="python_export:" + job_id, reason="One CSV export")
        # Reaching here means a signed receipt authorized THIS job, even if it left zero credits.
        # Do not ask result.feature() again after the final paid credit was consumed.
        with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="", dir=folder,
                                         prefix="export-", suffix=".tmp", delete=False) as output:
            temporary = Path(output.name)
            writer = csv.writer(output)
            writer.writerow(["Message"])
            writer.writerow(["'" + text if text.lstrip().startswith(("=", "+", "-", "@")) else text])
            output.flush()
            os.fsync(output.fileno())
        os.replace(temporary, target)  # Deterministic output; retry cannot create a second CSV job.
        db.execute("UPDATE jobs SET state='DONE' WHERE id=?", (job_id,))
        db.commit()
        return target
    except Exception:
        db.rollback()  # The earlier committed PENDING row survives an uncertain charge reply.
        raise
    finally:
        db.close()
        if temporary is not None:
            temporary.unlink(missing_ok=True)


def main():
    parser = argparse.ArgumentParser(description="Export one CSV for one usage credit.")
    parser.add_argument("--job", required=True, help="Persist and reuse this logical job name on retry.")
    parser.add_argument("--text", required=True)
    args = parser.parse_args()
    client = make_client()
    try:
        if not client.activated:
            result = client.activate(getpass.getpass("License key: "),
                                     input("Matching customer email: ").strip() or None)
            if not result.valid or not result.signature_valid:
                raise AccessDenied("This license could not be activated. Check it in KDPika.")
        print("Saved:", run_job(client, args.job, args.text, application_folder() / "credit-jobs"))
        print("Run the identical job and message again to reuse its output without another charge.")
    except InsufficientCreditsError:
        raise SystemExit("Not enough credits. Ask the publisher to add credits, then retry this same job.")
    except (CreditOperationError, AccessDenied, ValueError):
        raise SystemExit("The job was not completed. Check access or reconnect; retry the same job and message.")
    except Exception:
        raise SystemExit("The result is uncertain. Do not choose a new job name; retry the same job and message.")
    finally:
        client.close()


if __name__ == "__main__":
    main()

5. Use it in a web application

Import run_job into a backend worker. First verify your own user session, authorization and CSRF; associate the job with that authorized user/tenant in your real job table. Then call run_job(client, stable_job_id, text, private_folder) with server-controlled values. Never treat a user-supplied arbitrary job ID/license as permission to read another user's saved output.

The downloadable credit script is a single-customer local example. It does not add a public credit-consume route or a multi-tenant job system. Adapt it to the existing server-side account/job model, not browser code. SQLite serializes this tutorial's local jobs; a distributed backend needs its own transactional job ownership and idempotent downstream effects.

6. Read the balance correctly

pythonpython
from kdpika_licensing import CreditOperationError

try:
    balance = client.get_balance()
    show_balance(balance)
except CreditOperationError:
    show_message("We could not refresh your balance. Please reconnect.")
# show_balance/show_message are callbacks in your own UI.

get_balance() makes an online signed check; a replayed receipt's balance belongs to its original transaction. Do not use an old receipt to display today's balance. Credit actions do not have an offline fallback.

7. Know the boundary

The server deduplicates the charge; your application deduplicates its work. This CSV example uses a stable file and journal. If your job sends email, transfers money or calls another service, use that service's own idempotency/recovery mechanism too. A SQLite row cannot guarantee exactly-once external effects.

Keep the journal and license state private and persistent. Never clear them just to retry an uncertain charge. A customer portal can show balances and recent transactions, but has no credit-pack checkout, invoices or payment refund tools.

Python feature map · Credits API reference · Customer portal.

Was this page helpful?

Choose an answer

Do not include personal data, license keys or credentials. Retained for up to 30 days. This is not a support inbox.

Search documentation

Arrow keys to move, Enter to open, Escape to close. Search stays in your browser.

Cookie settings