Skip to content

REST API Reference

Reference for the PyLocket REST API. All endpoints are prefixed with /v1/.

Base URL: https://api.pylocket.com/v1/

This page covers the developer-facing API. Admin-only and internal endpoints are not documented here.


Authentication

PyLocket accepts two credential types. Both identify the same developer account.

1. Bearer token (JWT) for interactive and portal-style use:

Authorization: Bearer <ACCESS_TOKEN>

Obtain one from POST /v1/auth/login.

2. API key for machine-to-machine integrations (marketplace webhooks, scripts, CI):

X-API-Key: <YOUR_API_KEY>

Create or replace your key with POST /v1/auth/api-keys/rotate, or from the Developer Portal under Settings. The key is shown once, so store it immediately.

Send only one credential

The two schemes are evaluated in order: Authorization first, X-API-Key second. If you send both and the JWT is expired or invalid, the request fails with 401 and the API key is never tried. For server-to-server calls, send X-API-Key on its own.


Auth Endpoints

POST /v1/auth/register

Register a new developer account.

Request Body:

{
  "email": "dev@example.com",
  "password": "SecureP@ss123",
  "company_name": "My Company"
}

Response (201):

{
  "id": "dev_abc123",
  "email": "dev@example.com",
  "company_name": "My Company",
  "created_at": "2026-03-01T00:00:00Z"
}

POST /v1/auth/login

Authenticate and receive JWT tokens.

Request Body:

{
  "email": "dev@example.com",
  "password": "SecureP@ss123"
}

Response (200) — without 2FA:

{
  "access_token": "eyJhbGc...",
  "refresh_token": "eyJhbGc...",
  "token_type": "bearer",
  "expires_in": 3600
}

Response (200) — with 2FA:

{
  "mfa_required": true,
  "mfa_token": "mfa_abc123"
}

POST /v1/auth/login/verify-2fa

Complete login with a TOTP code.

Request Body:

{
  "mfa_token": "mfa_abc123",
  "code": "123456"
}

Response (200):

{
  "access_token": "eyJhbGc...",
  "refresh_token": "eyJhbGc...",
  "token_type": "bearer",
  "expires_in": 3600
}

POST /v1/auth/refresh

Refresh an expired access token.

Request Body:

{
  "refresh_token": "eyJhbGc..."
}

Response (200):

{
  "access_token": "eyJhbGc...",
  "token_type": "bearer",
  "expires_in": 3600
}

GET /v1/auth/me

Get the authenticated developer's profile.

Response (200):

{
  "id": "dev_abc123",
  "email": "dev@example.com",
  "company_name": "My Company",
  "two_factor_enabled": true,
  "created_at": "2026-03-01T00:00:00Z"
}

POST /v1/auth/api-keys/rotate

Create an API key, or replace the existing one. Despite the name, this is also how you create your first key — there is no separate create endpoint, and calling it without a key already on the account is valid.

Any previous key stops working immediately.

api_key is returned once and never again: only a SHA-256 hash is stored, so it cannot be recovered. Copy it when you see it. api_key_prefix is the first 8 characters, kept so surfaces can show which key is active without holding the secret.

Response (200):

{
  "api_key": "5DHiPOIe7qUWnk1S2jt7T_g2Tu256T0wGIx6jKZX_nXtCXYUh_Sr4A-UVUMEaQc2",
  "api_key_prefix": "5DHiPOIe"
}

POST /v1/auth/2fa/setup

Start 2FA setup. Returns a provisioning URI for authenticator apps.

Response (200):

{
  "provisioning_uri": "otpauth://totp/PyLocket:dev@example.com?secret=...",
  "backup_codes": ["abc123", "def456", "ghi789", "jkl012"]
}

POST /v1/auth/2fa/confirm

Confirm 2FA setup with a TOTP code.

Request Body:

{
  "code": "123456"
}

Response (200):

{
  "two_factor_enabled": true
}

POST /v1/auth/2fa/disable

Disable 2FA.

Request Body:

{
  "code": "123456"
}

GET /v1/auth/2fa/status

Check 2FA status.

Response (200):

{
  "two_factor_enabled": true,
  "backup_codes_remaining": 3
}

App Endpoints

GET /v1/apps

List all apps for the authenticated developer.

Response (200): paginated

{
  "data": [
    {
      "id": "3f8a1c22-9b4e-4d17-a6f0-2c5e7d90b114",
      "name": "MyApp",
      "build_platforms": ["win-x64", "linux-x64"],
      "obfuscation_level": "paranoid",
      "created_at": "2026-03-01T00:00:00Z"
    }
  ],
  "meta": { "page": 1, "page_size": 20, "total": 1 }
}

build_platforms lists the distinct platforms of READY builds. Fields are abridged; see the response of GET /v1/apps/{app_id} for the full shape.


POST /v1/apps

Create a new application.

Request Body:

{
  "name": "MyApp",
  "platform_targets": ["win-x64", "linux-x64", "macos-arm64"]
}

platform_targets is optional and informational: the operative platform is declared per build when you protect. python_versions (a list) is accepted the same way.

Response (201):

{
  "id": "3f8a1c22-9b4e-4d17-a6f0-2c5e7d90b114",
  "name": "MyApp",
  "platform_targets": ["win-x64", "linux-x64", "macos-arm64"],
  "obfuscation_level": "paranoid",
  "created_at": "2026-03-01T00:00:00Z"
}

GET /v1/apps/{app_id}

Get details for a specific app.


PATCH /v1/apps/{app_id}

Update an application. The method is PATCH (PUT returns 405). Platform targets are set at creation and are not updatable here.

Request Body:

{
  "name": "MyApp Pro",
  "description": "Updated description"
}

DELETE /v1/apps/{app_id}

Delete an application and all associated builds and licenses.

Warning: This action is irreversible.


Build Endpoints

POST /v1/apps/{app_id}/builds

Create a new protection build by uploading an artifact.

Request: JSON body. Upload is a two-step flow: this call returns a presigned upload_url; PUT the artifact bytes to it, then confirm the upload via POST /v1/apps/{app_id}/builds/{build_id}/confirm.

Field Type Description
version String Build version label (required)
artifact_type String e.g. exe, app, whl, zip, elf (required)
platform String Target platform, e.g. win-x64 (required)
python_version String Optional. Omit it and the protection engine detects the version from the artifact

Response (201):

{
  "id": "7c2d5e91-0a36-4b8f-9e14-8d63f2a70c55",
  "app_id": "3f8a1c22-9b4e-4d17-a6f0-2c5e7d90b114",
  "version": "1.0.0",
  "status": "pending",
  "upload_url": "https://...presigned...",
  "created_at": "2026-03-01T00:00:00Z"
}

GET /v1/apps/{app_id}/builds

List all builds for an app.

Query Parameters:

Parameter Description
status Filter by status
limit Max results (default: 20)
offset Pagination offset

GET /v1/apps/{app_id}/builds/{build_id}

Get build details including status.

Response (200):

{
  "id": "7c2d5e91-0a36-4b8f-9e14-8d63f2a70c55",
  "app_id": "3f8a1c22-9b4e-4d17-a6f0-2c5e7d90b114",
  "status": "ready",
  "platform": "win-x64",
  "python_version": "3.12",
  "obfuscation_level": "paranoid",
  "scan_status": "clean",
  "protected_size_bytes": 2457600,
  "protected_size_bytes": 3145728,
  "created_at": "2026-03-01T00:00:00Z",
  "completed_at": "2026-03-01T00:02:30Z"
}

GET /v1/apps/{app_id}/builds/{build_id}/download

Get a signed download URL for the protected artifact.

Response (200):

{
  "download_url": "https://cdn.pylocket.com/...",
  "expires_at": "2026-03-08T00:00:00Z"
}

The download URL is valid for 7 days.


License Endpoints

How licenses get created

There is no generic POST /v1/licenses. A license is issued by whichever distribution path you use, and each path has its own endpoint:

You want to Use Charged
Generate a license key directly from your backend (marketplace webhook, custom storefront) POST /v1/marketplace/licenses $4 per license
Hand out codes in bulk that customers redeem later (AppSumo-style launch) POST /v1/redemption/batches, redeemed via POST /v1/redeem $4 on redemption; codes are free to create
Sell through PyLocket-managed Stripe Checkout POST /v1/licenses/{license_id}/purchase-link, or an app Payment Link $4 per sale
Give someone a direct download link Direct links (see Distribute Your App) $4 on claim
Offer a free trial Trial links (see Free Trial Links) Free
License a Shopify App Store subscription The Partner Entitlement Webhook $4 once per merchant, at purchase or trial conversion

The endpoints below manage licenses that already exist.

POST /v1/licenses/activate

Activate an end-user license. Called by the protected application at runtime. Request and response details are handled automatically by the PyLocket runtime.

Error Responses:

Status Meaning
401 Invalid or revoked license key
403 Device limit exceeded
429 Velocity limit exceeded (too many activations)

POST /v1/licenses/refresh

Refresh a runtime token. Called periodically by the protected application. Request and response details are handled automatically by the PyLocket runtime.


GET /v1/licenses

List licenses for the authenticated developer.

Query Parameters:

Parameter Description
app_id Filter by app
status Filter by status: active, revoked, expired, demo
limit Max results (default: 20)
offset Pagination offset

POST /v1/licenses/{license_id}/revoke

Revoke a license immediately. The license stops activating on all devices.

Response (200):

{
  "id": "e7a51ccd-237b-46fc-86de-43f95e2e1115",
  "status": "revoked",
  "revoked_at": "2026-07-27T16:26:14.479123Z"
}

POST /v1/licenses/{license_id}/purchase-link

Create a Stripe Checkout URL so an end user can pay for an existing license. Requires an active Pro subscription, a connected Stripe account, and a Price ID configured on the app. See Stripe Connect Setup.

No request body. Response (200):

{
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_..."
}

Error Responses:

Status Meaning
400 The app has no Stripe Price ID configured
403 Pro subscription required, or Stripe account not connected
404 License not found

Marketplace Endpoints

For selling through third-party marketplaces (AppSumo, Gumroad, Paddle, LemonSqueezy) or your own storefront. Full walkthrough: Marketplace Distribution.

POST /v1/marketplace/licenses

Generate a license key directly. Designed for machine-to-machine use: your backend calls this when a marketplace webhook reports a sale. This is the endpoint to use when you need a license key now, in the response.

Authentication: X-API-Key (recommended) or Authorization: Bearer.

Rate limit: 60 requests per minute.

Request:

{
  "app_id": "15ed6311-6ef1-436e-8476-3bc287e44dfb",
  "channel": "appsumo",
  "customer_email": "buyer@example.com",
  "marketplace_order_id": "ORDER-12345",
  "license_config": {
    "device_limit": 3,
    "expiry_days": null,
    "license_type": "perpetual"
  }
}
Field Required Notes
app_id Yes UUID of an app you own
channel Yes One of stripe, appsumo, gumroad, paddle, lemonsqueezy, shopify, manual, api
customer_email No Stored on the license for your records
marketplace_order_id No Your order reference, max 255 characters
license_config.device_limit No Default 3, range 1 to 10000
license_config.expiry_days No null (default) means perpetual
license_config.license_type No perpetual (default), subscription, or trial

Response (201):

{
  "license_key": "AT6A-UPCT-1YNU-ONRR",
  "license_id": "e7a51ccd-237b-46fc-86de-43f95e2e1115",
  "app_name": "My App",
  "download_url": "https://api.pylocket.com/v1/downloads/<token>",
  "download_expires_at": "2026-08-01T12:30:00Z"
}

Store the license key when you receive it

license_key is returned only in this response. PyLocket stores a hash, so the plaintext key cannot be retrieved later.

download_url is populated only when the app has a READY build with a protected artifact; otherwise it is null. download_expires_at tells you when that link stops working — mint a fresh license (or use the delivery page) after it lapses.

How your end-user activates — the key must reach the app

The download_url gives you the bare protected application, not a key-injected installer. Your end-user needs the license_key and a way to supply it to the app, or the app will show "the application encountered a problem and needs to close" on launch. Any of these works, on every engine version:

  • license-key.txt beside the executable (simplest for automation): write a file named license-key.txt containing the key in the same folder as the .exe (or, for a macOS .app, next to the inner Contents/MacOS/<exe>). The runtime reads it automatically — no prompt.
  • PYLOCKET_LICENSE_KEY environment variable set to the key before launch.
  • The PyLocket Installer / delivery page (get.pylocket.com/download), which injects license-key.txt for the end-user — the zero-touch path.
  • On-launch prompt — apps protected on the current engine also prompt for the key if none of the above is present. (Very old builds may not prompt reliably on windowed apps; supply the key via one of the methods above, or re-protect to pick up the current prompt.)

Error Responses:

Status Meaning
401 Missing or invalid credentials
403 Free-tier license limit reached (10)
404 App not found, or not owned by you
429 Rate limit exceeded

Free-tier behavior

On the free tier every license is issued as a trial (is_trial=true), defaulting to 30 days unless you supply license_config.expiry_days (which is honored). license_config.license_type is ignored, so you cannot issue a perpetual license on the free tier. You are also capped at 10 licenses total, and the cap counts revoked licenses. Upgrade to Pro to issue perpetual licenses. The $4 license fee applies to Pro accounts.

No Stripe account required

This endpoint needs no Stripe Connect account and no Stripe account of your own. Sell through Gumroad, AppSumo, Paddle, your own storefront, or anywhere else; PyLocket bills you the $4 separately.


Redemption Endpoints

Generate codes in bulk now, and licenses are created only when customers redeem them. Creating codes is free; the $4 license fee applies at redemption. This is the usual choice for a marketplace launch where you must supply thousands of codes upfront.

POST /v1/redemption/batches

Create a batch of redemption codes.

Request:

{
  "app_id": "15ed6311-6ef1-436e-8476-3bc287e44dfb",
  "channel": "appsumo",
  "quantity": 500,
  "label": "AppSumo launch",
  "license_config": {"device_limit": 3, "license_type": "perpetual"}
}

quantity ranges from 1 to 10000.

Response (201):

{
  "batch_id": "...",
  "channel": "appsumo",
  "quantity": 500,
  "codes": ["PLR-XXXXXXXX-XXXX", "..."],
  "created_at": "2026-07-27T16:00:00Z"
}

Codes are returned once

Save the codes array immediately. PyLocket stores only a hash and an 8-character prefix, so the full codes cannot be recovered. The export endpoint returns prefixes only.


GET /v1/redemption/batches

List your redemption batches.

GET /v1/redemption/batches/{batch_id}

Batch detail, including redemption counts.

GET /v1/redemption/batches/{batch_id}/codes

List code prefixes and their redemption status.

GET /v1/redemption/batches/{batch_id}/export

Export the batch as CSV. Contains code prefixes, not full codes.

POST /v1/redemption/batches/{batch_id}/revoke

Revoke all unredeemed codes in the batch.


POST /v1/redeem

Redeem a code and receive a license. No authentication (your customer calls this, typically from your redemption page). Rate limited to 5 requests per minute per IP.

Request:

{
  "code": "PLR-XXXXXXXX-XXXX",
  "customer_email": "buyer@example.com"
}

Response (200):

{
  "license_key": "AT6A-UPCT-1YNU-ONRR",
  "license_id": "...",
  "app_name": "My App",
  "download_url": "https://...",
  "download_expires_at": "2026-08-03T16:00:00Z"
}

Error Responses:

Status Meaning
403 Developer's free-tier license limit reached
404 Code not found
409 Code already redeemed
410 Code expired or revoked

Partner Entitlement Webhook

License a subscription that lives on another platform (first supported partner channel: the Shopify App Store). Your service verifies the platform's webhooks, then forwards a normalized entitlement decision here; PyLocket grants, revokes, or reactivates the merchant's license to match. Full guide: Sell on Shopify.

POST /v1/public/partner-entitlements/{token}

Authentication: the per-app shared secret, sent in the X-MerchantOps-Secret header (alias: X-PyLocket-Partner-Secret) and verified in constant time. The {token} comes from the portal's Sell on the Shopify App Store card, which also generates the secret.

The secret is shown once

The portal displays the shared secret exactly once, at generation. Store it as an encrypted secret in your service. Rotation is available anytime; after rotating, the previous secret keeps working for 24 hours.

Rate limit: 120 requests per minute per IP. Bodies over 16 KB are rejected.

Request:

{
  "app": "your-app-slug",
  "shop": "example.myshopify.com",
  "topic": "app_subscriptions/update",
  "status": "active",
  "entitled": true,
  "subscription_name": "Pro",
  "event_id": "d9f8a1c2-...",
  "occurred_at": "2026-08-16T10:00:00Z",
  "trial": false,
  "test": false,
  "customer_email": "merchant@example.com"
}

entitled (JSON boolean), shop (a *.myshopify.com domain), event_id, and occurred_at are required; app must match the product key configured in the portal. trial: true events mint without the license fee (charged once at conversion); test: true events run the full flow but never bill.

Response (200):

{
  "ok": true,
  "action": "granted",
  "event_id": "d9f8a1c2-...",
  "license_key": "XXXX-XXXX-XXXX-XXXX",
  "delivery_url": "https://get.pylocket.com/d/...",
  "note": null
}

action is one of granted, reactivated, revoked, noop, duplicate, stale_skipped, unactionable. license_key and delivery_url are present on grant-type results, including duplicates of them, so retries are self-healing. A 200 is returned only after the change is durably saved.

Error Responses:

Status Meaning
400 Malformed JSON or invalid fields (permanent; do not retry)
401 Missing or wrong shared secret (permanent; do not retry)
404 Unknown token, or the webhook is disabled
413 Body over 16 KB
429 Rate limited; retry after the indicated delay
503 The webhook is enabled but no secret is configured

Retry only on 5xx, timeouts, and 429.

GET /v1/public/partner-entitlements/{token}/entitlement

Same authentication as the webhook. Query parameter: shop (the permanent *.myshopify.com domain). Returns the shop's current entitlement, strictly read-only:

{
  "ok": true,
  "shop": "example.myshopify.com",
  "entitled": true,
  "status": "active",
  "trial": false,
  "license_key": "XXXX-XXXX-XXXX-XXXX",
  "delivery_url": "https://get.pylocket.com/d/...",
  "expires_at": null
}

entitled folds in any trial expiry; a shop with no licence returns entitled: false with null fields. 400 for a missing or malformed shop.

POST /v1/public/partner-entitlements/{token}/delivery-email

Body: {"shop": "...", "email": "merchant@example.com"} stores a merchant-provided address on the shop's licence and sends the standard delivery email. {"shop": "...", "email": null} clears it. Setting the same, already-delivered address again returns {"action": "unchanged"} without a resend. 404 when the shop has no licence.

POST /v1/public/partner-entitlements/{token}/trial

Body: {"shop": "...", "customer_email": "...", "test": true} (all but shop optional). Mints a 14-day no-commitment trial licence, or echoes the shop's current state when a licence already exists. The route is idempotent by state — call it on every sign-in with any (or no) event_id. Responses carry action: trial_granted or noop (with the current state either way).

The shop is only the licence's identity: every trial rule is enforced at the end user's computer, not the store. The trial's 14-day clock starts at the app's first successful activation on a machine (until then the licence reports trial: true with expires_at: null), each machine can run a limited number of trials of the app (default one, whoever downloads them), and store events never end a trial — uninstalling leaves the trial running out its own clock, so reinstalling simply resumes it. The trial is never billed; the first entitled: true webhook event without the trial flag converts it, billing the one-time fee and lifting the trial window. These three companion endpoints are synchronous broker calls: treat 400/401/404/409 as permanent and alert.

Managing the integration

Action Endpoint
Enable (generates the URL token; generates the secret when none exists) POST /v1/apps/{app_id}/partner-webhook with {"product_key": "..."}
Rotate the secret (returned once; old secret valid 24h) POST /v1/apps/{app_id}/partner-webhook/rotate-secret
Disable event intake (URL and secret preserved) DELETE /v1/apps/{app_id}/partner-webhook

These use your normal developer authentication (X-API-Key or a session token). Enable and rotate require a Pro subscription; disable does not, so an integration can always be shut off.


Public Endpoints

GET /v1/public/pricing

Get public pricing information. No authentication required.

Response (200): amounts are in cents. Abridged; the live response also includes example_fees[], features[], tagline, and cta_text for rendering the pricing page.

{
  "annual_subscription_price_cents": 900,
  "annual_subscription_price_label": "$9.00/yr",
  "license_fee_cents": 400,
  "license_fee_label": "$4.00/license sold",
  "free_build_download_limit": 10,
  "storage_formula": "Base Fee + ceil(GB x S3 Rate x (1 + Margin) x 12 months)",
  "s3_cost_per_gb_month_cents": 2.3,
  "profit_margin_percent": 15.0,
  "registration_enabled": true
}

Read prices from this endpoint, do not hard-code them

The annual subscription price changes over time (existing subscribers keep the price they signed up at). Always read annual_subscription_price_cents rather than embedding a number.


Error Response Format

All errors follow a consistent format:

{
  "detail": "Human-readable error message",
  "error_code": "INVALID_LICENSE_KEY",
  "status_code": 401
}

Rate Limiting

All API endpoints are rate-limited. Rate limits vary by endpoint group (authentication, app management, build operations, license activation, and general API access).

Rate limit headers are included in all responses:

X-RateLimit-Limit: <limit>
X-RateLimit-Remaining: <remaining>
X-RateLimit-Reset: <unix-timestamp>

When the rate limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header.


Pagination

List endpoints support pagination:

Parameter Description Default
limit Max items per page 20
offset Number of items to skip 0

Response includes pagination metadata:

{
  "items": [...],
  "total": 150,
  "limit": 20,
  "offset": 0
}

See Also

Guide Covers
Marketplace Distribution Redemption codes and API provisioning, with Python and cURL examples
Stripe Connect Setup Selling through PyLocket-managed Stripe Checkout
Distribute Your App Direct links and delivery pages
Free Trial Links Time-limited trial distribution
Billing What each license path costs