Creator Documentation
API reference

Desktop Auth API

Use Modsy's browser-based device flow to authenticate a user and obtain the current entitlement for your approved desktop app registration.

On this page

Overview

The desktop flow has four phases:

  1. Start a login session with an S256 proof challenge.
  2. Open the returned Modsy verification URL in the system browser.
  3. Poll with the matching proof verifier until the user approves, denies, or the session expires.
  4. Store the issued opaque tokens securely and re-check product entitlement over time.

Authentication is not entitlement

A poll response with status: "approved" means the user signed in successfully. Unlock the product only after separately checking entitlement.access.

All requests and entitlement decisions go through Modsy. Do not call Patreon or Lemon Squeezy from the desktop app.

Base URL and endpoints

Production base URL:

https://modsy.io

| Purpose | Method | Path | | --- | --- | --- | | Start login | POST | /api/desktop/auth/start | | Poll login | POST | /api/desktop/auth/poll | | Refresh tokens | POST | /api/desktop/auth/refresh | | Get entitlement | GET | /api/desktop/entitlement | | Logout | POST | /api/desktop/auth/logout |

The browser approval endpoint is used by Modsy's /device page. Desktop clients should not call it directly.

Generate the S256 proof key

Generate a fresh verifier for every login. It must contain 43–128 characters from A-Z, a-z, 0-9, ., _, ~, or -. The challenge is the unpadded Base64URL encoding of the verifier's SHA-256 digest.

Example for .NET:

using System.Security.Cryptography;
using System.Text;

static string Base64Url(byte[] value) =>
    Convert.ToBase64String(value)
        .TrimEnd('=')
        .Replace('+', '-')
        .Replace('/', '_');

string codeVerifier = Base64Url(RandomNumberGenerator.GetBytes(64));
string codeChallenge = Base64Url(
    SHA256.HashData(Encoding.ASCII.GetBytes(codeVerifier))
);

Keep codeVerifier in memory until polling finishes. Do not log it, send it to the browser, or reuse it for another login.

Start login

POST /api/desktop/auth/start
Content-Type: application/json
{
  "app_id": "your-mod-installer",
  "app_version": "1.0.0",
  "device_name": "Windows PC",
  "device_id": "8c1b98aa-52dd-46d7-a44a-013acf26b8da",
  "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
  "code_challenge_method": "S256"
}

| Field | Required | Description | | --- | --- | --- | | app_id | Yes | The immutable public ID approved by Modsy. | | app_version | No | Your client version for audit and troubleshooting. | | device_name | No | A short user-facing label; do not include sensitive data. | | device_id | Yes | A stable random identifier for this installation. | | code_challenge | Yes | The Base64URL-encoded SHA-256 challenge. | | code_challenge_method | Yes | Must be S256. |

Response:

{
  "login_session_id": "sess_opaque_random_value",
  "verification_url": "https://modsy.io/device",
  "verification_uri_complete": "https://modsy.io/device?code=ABCD-1234",
  "user_code": "ABCD-1234",
  "expires_in": 900,
  "poll_interval_seconds": 5
}

Open verification_uri_complete in the system's default browser. Display user_code inside your app so the user can compare it with the browser page. Poll no faster than poll_interval_seconds and stop after expires_in.

Browser approval

Modsy handles approval at /device?code=.... The user must:

  • Sign in to Modsy in the browser.
  • Confirm that the browser code exactly matches the code displayed by your app.
  • Review the linked mod and publisher.
  • Explicitly approve or deny the request.

Approval may succeed even if the user does not have product access. Your client still needs to inspect the returned entitlement.

Poll login status

POST /api/desktop/auth/poll
Content-Type: application/json
{
  "login_session_id": "sess_opaque_random_value",
  "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}

Pending, denied, and expired responses are terminal except for pending:

{ "status": "pending" }
{ "status": "denied" }
{ "status": "expired" }

Approved response:

{
  "status": "approved",
  "access_token": "modsy_at_opaque_random_value",
  "refresh_token": "modsy_rt_opaque_random_value",
  "expires_in": 3600,
  "user": {
    "id": "user_123",
    "name": "User Name"
  },
  "entitlement": {
    "product": "your-mod-installer",
    "access": "active",
    "source": "patreon",
    "plan_name": "Supporter",
    "patreon_campaign_id": "campaign_123",
    "valid_until": null,
    "grace_until": null,
    "checked_at": "2026-09-04T10:00:00.000Z"
  }
}

The login session issues tokens only once. Store both tokens immediately in the operating system's secure credential store. Do not depend on an email field; new integrations receive only minimal account identity.

Refresh session

POST /api/desktop/auth/refresh
Content-Type: application/json
{
  "refresh_token": "modsy_rt_opaque_random_value",
  "device_id": "8c1b98aa-52dd-46d7-a44a-013acf26b8da"
}
{
  "access_token": "modsy_at_new_opaque_random_value",
  "refresh_token": "modsy_rt_new_opaque_random_value",
  "expires_in": 3600
}

Refresh tokens are rotated and bound to device_id. Atomically replace both stored tokens after every successful refresh. If refresh returns TOKEN_EXPIRED or TOKEN_REVOKED, clear local state and begin a new login.

Get current entitlement

GET /api/desktop/entitlement
Authorization: Bearer modsy_at_opaque_random_value
{
  "user": {
    "id": "user_123",
    "name": "User Name"
  },
  "entitlement": {
    "product": "your-mod-installer",
    "access": "active",
    "source": "lemon",
    "plan_name": "Premium Access",
    "patreon_campaign_id": null,
    "valid_until": "2026-10-04T10:00:00.000Z",
    "grace_until": null,
    "checked_at": "2026-09-04T10:00:00.000Z"
  }
}

Check entitlement:

  • When the application starts.
  • Before unlocking protected product features.
  • After refreshing an expired access token.
  • Periodically during a long-running session.

Treat the Modsy tokens as opaque strings. Never parse them to infer identity, expiry, or entitlement.

Logout

POST /api/desktop/auth/logout
Content-Type: application/json
{
  "refresh_token": "modsy_rt_opaque_random_value",
  "device_id": "8c1b98aa-52dd-46d7-a44a-013acf26b8da"
}
{ "ok": true }

Logout is idempotent. Clear all local auth state after it succeeds, including when the remote session was already revoked.

Entitlement values

| access | Client behavior | | --- | --- | | active | Unlock product access. | | grace | Unlock temporarily and show an appropriate access warning. | | no_access | Keep locked and direct the user to the official Modsy mod page. | | expired | Keep locked; the entitlement has ended. | | revoked | Keep locked and clear unusable local auth state when applicable. | | banned | Keep locked and show account-restriction messaging. |

| source | Meaning | | --- | --- | | patreon | Patreon-backed entitlement granted access. | | lemon | Lemon Squeezy-backed entitlement granted access. | | manual | A Modsy manual entitlement applies. | | none | No entitlement source currently grants access. |

Do not create client-side rules based on plan_name, provider IDs, prices, or tier names. Only access decides whether the product unlocks.

Errors

Errors use this shape:

{
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "The desktop access token has expired."
  }
}

| Code | Client response | | --- | --- | | AUTH_PENDING | Continue polling at the allowed interval. | | AUTH_DENIED | Stop and return to a signed-out state. | | AUTH_EXPIRED | Stop and offer to start a new login. | | TOKEN_EXPIRED | Refresh when possible; otherwise start a new login. | | TOKEN_REVOKED | Clear tokens and require a new login. | | SUBSCRIPTION_REQUIRED | Keep the product locked. | | SUBSCRIPTION_GRACE | Allow only according to the returned entitlement state. | | ACCOUNT_BANNED | Keep locked and show account-restriction messaging. | | APP_NOT_ALLOWED | Stop; the app registration is unavailable. | | RATE_LIMITED | Respect Retry-After and back off. | | INVALID_REQUEST | Stop and correct the client request. | | SERVER_ERROR | Keep locked and retry later with bounded backoff. |

Unknown errors must fail closed. Never unlock because Modsy could not be reached or returned an unfamiliar response.

Security checklist

  • Use HTTPS and the production base URL exactly as documented.
  • Generate a new verifier and challenge for every login.
  • Never embed a reusable client secret in a desktop executable.
  • Never request or store Patreon or Lemon Squeezy credentials or identifiers.
  • Store refresh tokens in the OS secure credential store.
  • Never log tokens, proof verifiers, or include them in crash reports.
  • Rotate local refresh tokens after every successful refresh.
  • Use a random stable installation ID that contains no personal information.
  • Unlock only for active or grace.
  • Fail closed on network, parsing, server, and unknown entitlement errors.
  • Publish signed installers through the official linked Modsy mod page.
  • Clear local state on logout or TOKEN_REVOKED.

Cookie choices

We use necessary cookies for security. Optional cookies remember preferences, measure activity, and support ads. Privacy Policy