Documentation

Marketplace licenses

Use LoginSign as the license layer for AppSumo and similar lifetime-deal marketplaces. You configure variants once in the Console. The marketplace reports a purchase or activation, the buyer signs in on LoginSign, the license binds to that account automatically, and your product learns about it through webhooks or the inventory API.

1. Who this is for

Use this guide if you sell a LoginSign-authenticated product on a deal marketplace (AppSumo, Dealify, PitchGround, StackSocial, or your own shop) and you do not want buyers to type a license code. It is also the contract if your backend or a marketplace integration must create, look up, redeem, or revoke codes without a Console session.

If you only issue codes yourself (CSV, Console generate, or assign from a user), start with Licenses. If your own billing system is the source of truth and LoginSign should only mirror plan state, use Status instead.

2. What LoginSign does (and does not)

LoginSign is the identity and entitlement store. It does not create AppSumo listings, charge buyers, or replace Stripe. After a marketplace sale, LoginSign stores the key, connects the buyer to your app, and tells your backend what changed.

  • Marketplace → LoginSign: AppSumo webhook, generic events API, or code import.
  • Buyer → LoginSign: hosted activation page or OAuth with license_code. No manual code entry.
  • LoginSign → your app: outbound webhook events on the same webhookUrl as account deletion, plus inventory pull.
  • Your backend ↔ LoginSign: Basic Auth read and write APIs.

3. Choose a path

You sell onUseBuyer experience
AppSumoHosted Licensing v2 connectorAppSumo Activate → LoginSign sign-in → your app
Dealify, PitchGround, StackSocial, own shopPartner APIs + activation URLRedirect to /activate/:appId?code=
Your own landing page already starts OAuthlicense_code on /oauth/authorizeConsent, then auto-redeem on the new connection

You can use both paths on the same app. AppSumo credentials live in Console → Licenses → Marketplaces. Generic APIs always work with your existing client_id and client_secret.

4. Console setup

  1. Create license variants on /console/licenses (for example Tier 1, Tier 2). Marketplace keys default to the variant's duration; AppSumo lifetime deals usually map to LIFETIME variants.
  2. Open the Marketplaces card on the same page. Copy the webhook URL, OAuth redirect URL, activation URL, and events API.
  3. For AppSumo: paste those URLs into the Partner Portal, then save the generated client ID, client secret, and API key back in LoginSign.
  4. Map AppSumo tiers 1, 2, 3 to variants. Unmapped tiers create or reuse a variant named Tier n.
  5. Set a success redirect URL. It must already be one of the app's OAuth redirect URIs. After activation, LoginSign starts /oauth/authorize toward that URL so the buyer lands in your product.
  6. Configure webhookUrl in Console → Settings if you want license_activated, license_updated, and license_deactivated posted to your backend.
  7. Search the license table by marketplace key when marketplace support asks for a lookup. AppSumo looks up by license key, not by email.

5. AppSumo Licensing v2 (hosted)

LoginSign hosts the two URLs AppSumo validates in the Partner Portal. Replace :appId with your LoginSign application id (also your OAuth client_id):

POST https://id.bookmer.com/api/marketplace/appsumo/:appId/webhook
GET  https://id.bookmer.com/marketplace/appsumo/:appId/callback

URL validation

  • AppSumo first calls the OAuth redirect with an empty GET. LoginSign answers 200 OK and the body OK.
  • Test webhooks include "test": true. LoginSign answers { "event": "activate", "success": true } and does not write database rows.
  • After both URLs validate, AppSumo generates OAuth keys. Copy them into Console → Licenses → Marketplaces.

Live webhook security

Live events (when test is not true) require a valid X-Appsumo-Signature. LoginSign computes HMAC-SHA256 of timestamp + raw body with your AppSumo API key and compares it to the header. Also send X-Appsumo-Timestamp. If no API key is saved yet, only test events are accepted.

X-Appsumo-Timestamp: 1318781876406
X-Appsumo-Signature: <hmac-sha256 hex of timestamp + raw JSON body>

Webhook events

AppSumo POSTs JSON. LoginSign always replies with the AppSumo success shape when the event is accepted:

{ "event": "activate", "success": true }
EventWhat LoginSign does
purchaseCreates or updates an unused code for license_key.
activateUpserts the key. The buyer is not bound yet — binding happens on OAuth / /activate.
upgrade / downgradeCreates or updates the new key, maps the new tier, expires prev_license_key, and moves an existing connection to the new key. Sends license_updated if a user was already bound.
deactivateExpires the key (refund). Active add-on rows with that parent key also expire. Sends license_deactivated.
migrateStores add-on metadata only (parent_license_key, unit_quantity, partner_plan_name). No extra product in v1.

Example live body:

{
  "license_key": "3794577C-3DBC-11EC-9BBC-0242AC130002",
  "event": "purchase",
  "license_status": "inactive",
  "tier": 2,
  "prev_license_key": null,
  "parent_license_key": null,
  "unit_quantity": 1,
  "partner_plan_name": null,
  "test": false
}

upgrade / downgrade send the new license_key plus prev_license_key. Add-on migrate events send parent_license_key, unit_quantity, and partner_plan_name.

Buyer activate (OAuth callback)

  1. The buyer clicks Activate on AppSumo and is redirected to the LoginSign callback with ?code=.
  2. LoginSign exchanges that code at https://appsumo.com/openid/token/ and fetches the key from https://appsumo.com/openid/license_key/.
  3. LoginSign upserts the license, then redirects the browser to /activate/:appId?license_key=….
  4. The buyer signs in (or is already signed in), LoginSign creates the app connection if needed, redeems the key, posts license_activated if this is a first bind, then starts OAuth into your product.

The AppSumo redirect_uri you register must be exactly the callback URL shown in the Console (including scheme and host).

6. Generic marketplaces (partner API)

Same credential model as License Sync and Status: HTTP Basic Auth, client_id:client_secret, and client_id must equal :appId.

Authorization: Basic base64(client_id:client_secret)
Content-Type: application/json

Generate codes

POST /api/applications/:appId/licenses/codes/generate

{
  "variantId": "var_123",
  "duration": "LIFETIME",
  "quantity": 100
}

// 201
{ "created": [{ "id": "lic_1", "code": "LS-NRS97-RPPEX-SSCZF-Z7HMB" }] }

Quantity is clamped to 1..50000. Duration is MONTH, YEAR, or LIFETIME (default MONTH). The Console generate API still exists for the same job with a session cookie.

Import codes

POST /api/applications/:appId/licenses/codes/import

{
  "variantId": "var_123",
  "duration": "LIFETIME",
  "codes": [
    "DEAL-PLATFORM-KEY-1",
    { "code": "DEAL-PLATFORM-KEY-2", "variantId": "var_999", "duration": "YEAR" }
  ]
}

// 201
{ "createdCount": 2, "skippedCount": 0, "created": [{ "id": "lic_1", "code": "DEAL-PLATFORM-KEY-1" }] }

Max 5000 rows per request. Invalid or duplicate values are skipped. This is the Basic Auth equivalent of Console CSV upload.

Lookup one code

GET /api/applications/:appId/licenses/:code

{
  "code": "3794577C-3DBC-11EC-9BBC-0242AC130002",
  "state": "ACTIVE",
  "duration": "LIFETIME",
  "source": "MARKETPLACE",
  "marketplaceProvider": "APPSUMO",
  "marketplaceLicenseKey": "3794577C-3DBC-11EC-9BBC-0242AC130002",
  "marketplaceTier": 2,
  "marketplaceStatus": "active",
  "parentLicenseKey": null,
  "unitQuantity": null,
  "partnerPlanName": null,
  "variant": { "id": "var_123", "name": "Tier 2" },
  "ownerUserId": "clx...",
  "activatedAt": "2026-09-13T10:00:00.000Z",
  "expiresAt": null,
  "createdAt": "2026-09-13T09:55:00.000Z"
}

:code matches the stored code or marketplaceLicenseKey (case insensitive). Use this when marketplace support sends a key, not an email.

Redeem for a known user

Use this when your backend already signed the user in and has their LoginSign globalId. The user must already have an active connection to your app. If they do not, send them through /activate/:appId instead.

POST /api/applications/:appId/licenses/:code/redeem

{ "globalId": "AB1234" }

// 200
{ "ok": true, "alreadyActive": false, "subscription": { ... } }

Revoke

POST /api/applications/:appId/licenses/:code/revoke

{ "ok": true, "revoked": true, "code": "DEAL-PLATFORM-KEY-1" }

Sets state to EXPIRED and posts license_deactivated when the code was active.

Generic marketplace events

Same event names as AppSumo. Use this when the marketplace can POST JSON to you (or you forward their webhook).

POST /api/applications/:appId/marketplace/events

{
  "event": "purchase",
  "license_key": "DEAL-PLATFORM-KEY",
  "tier": 1,
  "test": false,
  "prev_license_key": null,
  "parent_license_key": null,
  "unit_quantity": 1,
  "partner_plan_name": "Pro LTD"
}

Events: purchase, activate, upgrade, downgrade, deactivate, migrate. upgrade / downgrade should send prev_license_key. test: true never writes data.

7. Activation without typing a code

Point the marketplace “activate” or “get access” button at:

https://id.bookmer.com/activate/:appId?code={CODE}
https://id.bookmer.com/activate/:appId?license_key={KEY}
  1. GET /api/marketplace/activate/:appId loads a preview (app name, variant, state).
  2. If the visitor is signed out, they sign in with returnUrl back to this page.
  3. POST /api/marketplace/activate/:appId (session cookie) creates or restores the app connection, redeems the code, then returns redirectUrl.
  4. The browser follows redirectUrl into /oauth/authorize using your success redirect URL (or the first registered redirect URI).

Your own landing page can skip /activate and start OAuth with the same auto-redeem:

GET https://id.bookmer.com/oauth/authorize?
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://yourapp.com/callback&
  response_type=code&
  license_code=LS-NRS97-RPPEX-SSCZF-Z7HMB

After consent, LoginSign redeems license_code on that connection. Do not pass license_code again if activation already succeeded on /activate — the code is already bound. A failed redeem does not block OAuth; the user still reaches your app and you can read inventory or subscriptions.

Manual redeem in the LoginSign dashboard (POST /api/user/licenses/redeem) still requires an existing connection. The hosted activation page is the flow that creates the connection first. Details: Licenses and OAuth Flow.

8. Outbound webhooks (LoginSign → your app)

Set webhookUrl in Console → Settings. LoginSign POSTs JSON. Failed deliveries are logged and do not roll back the license change. Respond with 2xx.

{
  "type": "license_activated",
  "applicationId": "app_123",
  "code": "LS-NRS97-RPPEX-SSCZF-Z7HMB",
  "marketplaceLicenseKey": "3794577C-3DBC-11EC-9BBC-0242AC130002",
  "globalId": "AB1234",
  "connectionId": "conn_123",
  "variant": { "id": "var_123", "name": "Tier 2" },
  "duration": "LIFETIME",
  "state": "ACTIVE",
  "tier": 2,
  "occurredAt": "2026-09-13T10:00:00.000Z"
}
  • license_activated — first successful redeem (user, partner redeem, or Console assign).
  • license_updated — upgrade or downgrade moved an already-bound user to a new key or tier.
  • license_deactivated — revoke, refund, or marketplace deactivate.

The same URL also receives connection_deleted, user_account_deleted, and email_changed. Branch on type. You can send a sample license event from Console → Settings → test webhook. See Account Deletion.

9. Inventory and sources

Pull every code, including marketplace keys, with GET /api/applications/:appId/licenses. Each row includes source (MANUAL, MARKETPLACE, STATUS_SYNC), marketplaceProvider, marketplaceLicenseKey, and marketplaceTier. Full field list: License Sync.

10. Errors

  • 401 unauthorized — Basic Auth header missing.
  • 401 invalid_client — wrong secret, or invalid AppSumo signature on a live webhook.
  • 403 forbiddenclient_id does not match appId.
  • 400 — missing license_key / variantId / globalId, or success redirect not in the app redirect list.
  • 404 — unknown app, code, or globalId without a connection.
  • 409 — code already used by another user.

11. AppSumo go-live checklist

  1. Create variants and map tiers in Console → Licenses → Marketplaces.
  2. Register webhook + OAuth URLs in the AppSumo Partner Portal and confirm both validate.
  3. Save AppSumo client ID, secret, and API key in LoginSign.
  4. Set success redirect URL and app webhookUrl.
  5. Buy with AppSumo developer credits, click Activate, sign in on LoginSign, and confirm you land in the product with an ACTIVE license.
  6. Confirm your backend received license_activated or that inventory pull shows the key and ownerGlobalId.
  7. Test upgrade / downgrade / refund in AppSumo and confirm LoginSign state plus outbound events.

12. Related docs