{"openapi":"3.1.0","info":{"title":"Lynx Reseller API","version":"v1","description":"This API gives your storefront or your bot what it needs to sell Lynx packages.\nRead your balance. Read your prices next to the customer prices. Generate keys,\nand get a claim link with each key.\n\nTwo words in this document look similar. An **API key** is the credential that\nidentifies your integration. A **key** is a license that you sell to a customer.\n\n## Get started\n\n1. Open the dashboard. Go to **Reseller → API**.\n2. Create an API key. Copy it. The dashboard shows it one time only.\n3. Store the API key on your server. Use an environment variable.\n4. Call `GET /api/v1/me` with the API key. This confirms that the API key works.\n5. Call `GET /api/v1/products` to read your prices.\n6. Call `POST /api/v1/keys` to buy keys and get their claim links.\n\n## Authentication\n\nSend your API key in the `Authorization` header of each request. The\n`/openapi.json` endpoint is the one exception.\n\n```\nAuthorization: Bearer lyx_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nThe server keeps only a hash of your API key. No page can show the API key to\nyou again. If you lose an API key, revoke it and create another one.\n\nAn API key can spend your balance. Treat it like a password. Keep it on your\nserver. Do not put it in a browser, in an app bundle, or in a public repository.\n\n## Scopes\n\nEach API key holds a set of scopes. A scope permits one group of operations.\nThe server refuses a request that is outside the scopes of the API key, and\nanswers `403 forbidden`.\n\nGive each integration only the scopes that it needs. A price bot needs\n`catalog:read` and nothing more.\n\n| scope | permits |\n| --- | --- |\n| `account:read` | Read your reseller profile, your group, and your discount |\n| `balance:read` | Read your balance and your credit limit |\n| `catalog:read` | Read packages, plans, your prices, and customer prices |\n| `keys:read` | List and read the keys in your inventory |\n| `keys:write` | Generate keys |\n| `orders:read` | Read your purchase history |\n\n## Money and idempotency\n\n`POST /api/v1/keys` charges your balance.\n\nThe endpoint is idempotent on the `idempotencyKey` field. If you send the same\nvalue again, the server charges you one time. The response holds the same keys\nand `alreadyMinted: true`.\n\nMake one idempotency key for each order. Use that same value for each retry of\nthat order. **A new value on each attempt charges you two times.**\n\n## Claim links\n\nEach key in a response has a `claimUrl` field. Give the customer the key, or\nthe link, or both.\n\nThe link opens a claim page. The page shows the package, your store name, and\nyour message. The customer then redeems the key. Gift codes use this same page,\nso one flow serves both.\n\nSet `senderLabel` and `message` when you generate a batch. These two fields\ncontrol what the customer sees. You can change both later in the dashboard.\n\n## Errors\n\nA failed request returns this shape. The HTTP status matches the code.\n\n```json\n{ \"error\": { \"code\": \"invalid_request\", \"message\": \"quantity must be a number\" } }\n```\n\n| code | status | meaning |\n| --- | --- | --- |\n| `unauthorized` | 401 | The API key is missing, malformed, unknown, or revoked. |\n| `forbidden` | 403 | The API key is valid, but it does not hold the necessary scope. |\n| `not_found` | 404 | The endpoint does not exist, or the key is not in your inventory. |\n| `invalid_request` | 400 | A field is missing, or its value is out of range. |\n| `rate_limited` | 429 | You sent too many requests. The message tells you when to retry. |\n| `server_error` | 500 | The server failed. A retry of an idempotent request is safe. |\n\n## Rate limits\n\nEach API key holds two budgets. Read requests and write requests do not share a\nbudget.\n\n- Read: 600 each hour, with a burst of 120.\n- Write: 120 each hour, with a burst of 30.\n\nDo not share one API key between integrations that do not relate to each other.\nCreate a second API key instead.\n\n## Pagination\n\nList endpoints accept two query parameters. `limit` sets the page size, from 1\nto 200. `cursor` continues a previous listing.\n\nA list response holds three fields: `data`, `hasMore`, and `nextCursor`.\n\nTo read the next page, send `nextCursor` as the `cursor` parameter. Stop when\n`hasMore` is false.","contact":{"name":"Lynx support","url":"https://api.lynxcheats.com/contact"}},"servers":[{"url":"https://api.lynxcheats.com/api/v1","description":"Reseller API"}],"tags":[{"name":"Account","description":"Your profile and your balance."},{"name":"Catalog","description":"Packages, plans, and both price levels."},{"name":"Keys","description":"Generate and read the keys you sell."},{"name":"Orders","description":"What you bought."}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Your reseller API key. Create one in the dashboard under Reseller → API."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}},"Key":{"type":"object","properties":{"key":{"type":"string","example":"LYNX-A4KD-92JM-P3QR-7WTX"},"claimUrl":{"type":["string","null"],"description":"Give this link to the customer in place of the raw key.","example":"https://m.lynxcheats.com/redeem?code=LYNX-A4KD-92JM-P3QR-7WTX"},"status":{"type":"string","enum":["dormant","active","expired","revoked"]},"productId":{"type":["string","null"]},"packageName":{"type":["string","null"]},"priceId":{"type":["string","null"]},"planName":{"type":["string","null"]},"platform":{"oneOf":[{"type":"string","enum":["ios","android"]},{"type":"null"}],"description":"null means that the key works on each platform of the package."},"durationDays":{"type":"integer","description":"How many days the license runs. 0 means that it never expires."},"deviceLimit":{"type":"integer","description":"How many devices the key permits. 0 means no limit."},"devicesUsed":{"type":"integer"},"costCents":{"type":"integer","description":"The amount you paid, in cents."},"retailCents":{"type":"integer","description":"The customer price when the key was made, in cents."},"currency":{"type":"string","example":"usd","description":"USD for new financial operations; historical records retain their original currency."},"batchId":{"type":"string"},"createdAt":{"type":"integer","description":"Unix time in milliseconds."},"activatedAt":{"type":["integer","null"]},"expiresAt":{"type":["integer","null"]},"note":{"type":["string","null"]},"senderLabel":{"type":["string","null"]},"message":{"type":["string","null"]}}},"MintedKey":{"type":"object","properties":{"key":{"type":"string"},"claimUrl":{"type":["string","null"]}}},"Account":{"type":"object","properties":{"id":{"type":"string"},"uid":{"type":"integer"},"username":{"type":["string","null"]},"role":{"type":"string"},"keyPrefix":{"type":["string","null"],"description":"The four letters at the start of the keys you make. It is null if you do not have one."},"group":{"type":["object","null"],"properties":{"name":{"type":"string"},"discountBps":{"type":"integer","description":"Your discount in basis points. 4000 means 40 percent below the customer price."},"perPlanPricing":{"type":"boolean","description":"true means that your group sets its own price for some plans. The group discount is then not the full picture. Read /products for the real numbers."}}}}},"Balance":{"type":"object","properties":{"balanceCents":{"type":"integer","description":"Your cash balance, in cents."},"creditLimitCents":{"type":"integer","description":"How far below zero you can go, in cents."},"availableCents":{"type":"integer","description":"The sum of balanceCents and creditLimitCents. This is what you can spend now."},"currency":{"type":"string","example":"usd","description":"USD for new financial operations; historical records retain their original currency."}}},"Package":{"type":"object","properties":{"productId":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"platforms":{"type":"array","items":{"type":"string","enum":["ios","android"]}},"plans":{"type":"array","items":{"type":"object","properties":{"priceId":{"type":"string"},"name":{"type":"string"},"platform":{"type":"string","enum":["ios","android"]},"durationDays":{"type":"integer"},"currency":{"type":"string"},"customerPriceCents":{"type":"integer","description":"The price a customer pays on the storefront, in cents."},"yourPriceCents":{"type":"integer","description":"Your cost for this plan, in cents. Sell above it to make a margin."},"discountBps":{"type":"integer","description":"Your discount below the customer price, in basis points."}}}}}},"Order":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string"},"provider":{"type":"string"},"amountCents":{"type":["integer","null"]},"currency":{"type":["string","null"]},"quantity":{"type":["integer","null"]},"batchId":{"type":["string","null"]},"productId":{"type":["string","null"]},"priceId":{"type":["string","null"]},"createdAt":{"type":"integer"}}}}},"security":[{"apiKey":[]}],"paths":{"/me":{"get":{"tags":["Account"],"summary":"Read your reseller profile","description":"Returns your uid, your username, your key prefix, and your reseller group. Call this endpoint once at start-up. It confirms that an API key works, and it tells you your discount.","operationId":"getMe","responses":{"200":{"description":"Your reseller profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"401":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/balance":{"get":{"tags":["Account"],"summary":"Read your balance","description":"Read this endpoint before you generate keys. The `availableCents` field already includes your credit limit. A purchase succeeds if its total is at or below that number.","operationId":"getBalance","responses":{"200":{"description":"Your balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}},"401":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/products":{"get":{"tags":["Catalog"],"summary":"Read packages, plans, and both prices","description":"Returns each active package with the plans that you can buy. Each plan holds `customerPriceCents` and `yourPriceCents`. `customerPriceCents` is the storefront price. `yourPriceCents` is your cost. Your bot can set its own prices from these two numbers, and it does not need a hard-coded discount. The list holds only the plans that are public on a platform you sell.","operationId":"listProducts","responses":{"200":{"description":"The packages you can buy","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Package"}}}}}}},"401":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/keys":{"get":{"tags":["Keys"],"summary":"List your keys","description":"Returns your keys, newest first. Use the `status` parameter to select one state. Unsold stock has the status `dormant`. A redeemed key has the status `active`.","operationId":"listKeys","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200},"description":"How many rows one page holds, from 1 to 200."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` value from the previous page."},{"name":"status","in":"query","schema":{"type":"string","enum":["dormant","active","expired","revoked"]},"description":"Return only the keys in this state."},{"name":"productId","in":"query","schema":{"type":"string"},"description":"Return only the keys for this package."},{"name":"from","in":"query","schema":{"type":"integer"},"description":"Return only the keys made at or after this unix time in milliseconds."},{"name":"to","in":"query","schema":{"type":"integer"},"description":"Return only the keys made at or before this unix time in milliseconds."}],"responses":{"200":{"description":"One page of keys","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Key"}},"hasMore":{"type":"boolean"},"nextCursor":{"type":["string","null"]}}}}}},"400":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Keys"],"summary":"Generate keys","description":"Charges your balance and makes up to 500 keys of one plan. The response holds each key and its claim link.\n\nThis endpoint is idempotent on `idempotencyKey`. Use the same value for each retry of one order. The server then charges you one time. A repeat request returns the original keys and `alreadyMinted: true`.\n\n`senderLabel` and `message` control what the customer sees on the claim page. Both are optional. You can change both later in the dashboard.","operationId":"generateKeys","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["priceId","quantity","idempotencyKey"],"properties":{"priceId":{"type":"string","description":"Take this value from `GET /products`."},"quantity":{"type":"integer","minimum":1,"maximum":500},"idempotencyKey":{"type":"string","description":"Your own id for this order, 8 to 64 characters. Use the same value for each retry."},"senderLabel":{"type":"string","maxLength":40,"description":"The sender name the claim page shows. The default is your account name."},"message":{"type":"string","maxLength":280,"description":"A note that the customer reads before redemption."},"note":{"type":"string","description":"Private to you. The customer never sees it."}}}}}},"responses":{"201":{"description":"The keys the server made","content":{"application/json":{"schema":{"type":"object","properties":{"orderId":{"type":"string"},"batchId":{"type":"string"},"quantity":{"type":"integer"},"unitCostCents":{"type":"integer","description":"The cost of one key, in cents."},"totalCents":{"type":"integer","description":"The cost of the batch, in cents."},"balanceAfterCents":{"type":["integer","null"],"description":"Your balance after the charge, in cents."},"alreadyMinted":{"type":"boolean","description":"true means that this idempotencyKey was already used. The server did not charge you again."},"keys":{"type":"array","items":{"$ref":"#/components/schemas/MintedKey"}}}}}}},"400":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/keys/{key}":{"get":{"tags":["Keys"],"summary":"Read one key","description":"Returns a key from your own inventory. Any other key returns 404, even if that key exists.","operationId":"getKey","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"The key, with or without its dashes."}],"responses":{"200":{"description":"One key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Key"}}}},"401":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/orders":{"get":{"tags":["Orders"],"summary":"Read your purchase history","description":"Returns your orders, newest first. The `batchId` field connects an order to the keys that it made. Use `batchId` to reconcile a charge against `GET /keys`.","operationId":"listOrders","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200},"description":"How many rows one page holds, from 1 to 200."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The `nextCursor` value from the previous page."}],"responses":{"200":{"description":"One page of orders","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Order"}},"hasMore":{"type":"boolean"},"nextCursor":{"type":["string","null"]}}}}}},"400":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The server refused the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"tags":["Account"],"summary":"Read this document","description":"This is the one endpoint that does not need an API key.","operationId":"getOpenApi","security":[],"responses":{"200":{"description":"This OpenAPI document"}}}}},"x-scopes":[{"name":"account:read","description":"Read your reseller profile, your group, and your discount"},{"name":"balance:read","description":"Read your balance and your credit limit"},{"name":"catalog:read","description":"Read packages, plans, your prices, and customer prices"},{"name":"keys:read","description":"List and read the keys in your inventory"},{"name":"keys:write","description":"Generate keys"},{"name":"orders:read","description":"Read your purchase history"}]}