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
webhookUrlas account deletion, plus inventory pull. - Your backend ↔ LoginSign: Basic Auth read and write APIs.
3. Choose a path
| You sell on | Use | Buyer experience |
|---|---|---|
| AppSumo | Hosted Licensing v2 connector | AppSumo Activate → LoginSign sign-in → your app |
| Dealify, PitchGround, StackSocial, own shop | Partner APIs + activation URL | Redirect to /activate/:appId?code= |
| Your own landing page already starts OAuth | license_code on /oauth/authorize | Consent, 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
- 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 toLIFETIMEvariants. - Open the Marketplaces card on the same page. Copy the webhook URL, OAuth redirect URL, activation URL, and events API.
- For AppSumo: paste those URLs into the Partner Portal, then save the generated client ID, client secret, and API key back in LoginSign.
- Map AppSumo tiers
1,2,3to variants. Unmapped tiers create or reuse a variant namedTier n. - Set a success redirect URL. It must already be one of the app's OAuth redirect URIs. After activation, LoginSign starts
/oauth/authorizetoward that URL so the buyer lands in your product. - Configure
webhookUrlin Console → Settings if you wantlicense_activated,license_updated, andlicense_deactivatedposted to your backend. - 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 OKand the bodyOK. - 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 }| Event | What LoginSign does |
|---|---|
purchase | Creates or updates an unused code for license_key. |
activate | Upserts the key. The buyer is not bound yet — binding happens on OAuth / /activate. |
upgrade / downgrade | Creates 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. |
deactivate | Expires the key (refund). Active add-on rows with that parent key also expire. Sends license_deactivated. |
migrate | Stores 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)
- The buyer clicks Activate on AppSumo and is redirected to the LoginSign callback with
?code=. - LoginSign exchanges that code at
https://appsumo.com/openid/token/and fetches the key fromhttps://appsumo.com/openid/license_key/. - LoginSign upserts the license, then redirects the browser to
/activate/:appId?license_key=…. - The buyer signs in (or is already signed in), LoginSign creates the app connection if needed, redeems the key, posts
license_activatedif 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}GET /api/marketplace/activate/:appIdloads a preview (app name, variant, state).- If the visitor is signed out, they sign in with
returnUrlback to this page. POST /api/marketplace/activate/:appId(session cookie) creates or restores the app connection, redeems the code, then returnsredirectUrl.- The browser follows
redirectUrlinto/oauth/authorizeusing 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 marketplacedeactivate.
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 forbidden—client_iddoes not matchappId.400— missinglicense_key/variantId/globalId, or success redirect not in the app redirect list.404— unknown app, code, orglobalIdwithout a connection.409— code already used by another user.
11. AppSumo go-live checklist
- Create variants and map tiers in Console → Licenses → Marketplaces.
- Register webhook + OAuth URLs in the AppSumo Partner Portal and confirm both validate.
- Save AppSumo client ID, secret, and API key in LoginSign.
- Set success redirect URL and app
webhookUrl. - Buy with AppSumo developer credits, click Activate, sign in on LoginSign, and confirm you land in the product with an
ACTIVElicense. - Confirm your backend received
license_activatedor that inventory pull shows the key andownerGlobalId. - Test upgrade / downgrade / refund in AppSumo and confirm LoginSign state plus outbound events.
12. Related docs
- Licenses — variants, Console generate/upload, assign, user redeem
- License Sync — inventory pull
- Status — when your billing system is the source of truth
- OAuth Flow — authorize, token,
license_code - Synchronisation — webhook overview
- API Reference — route index