openapi: 3.1.0 info: title: NexoLink Publisher API version: 1.0.0 description: | Official REST API for the NexoLink URL Shortener Network. Publishers can automate short link creation, manage existing links, fetch real-time earnings, and inspect traffic analytics. ### Four Integration Methods: 1. **REST API v1 (JSON)**: Full programmatic CRUD and analytics with Bearer token authentication. 2. **Quick API (GET)**: Legacy, bot, and plain-text compatible link shortening (`/api/{key}?s={url}`). 3. **Full-Page Script**: Automated client-side outbound link conversion for websites (`/sdk/fullpage.js`). 4. **Plugins & SDKs**: Pre-built wrappers for WordPress and Telegram bots. contact: name: NexoLink Developer Support url: https://nexolink.cc/contact email: support@nexolink.cc license: name: Proprietary servers: - url: https://nexolink.cc description: Production API Server - url: http://127.0.0.1:8000 description: Local Development Server paths: # --------------------------------------------------------------------------- # Quick API # --------------------------------------------------------------------------- /api/{api_key}: get: summary: Quick API link shortening description: | Simplified GET endpoint compatible with legacy link shortener bots and community scripts. Returns plain-text short URL by default, or JSON when `format=json`. operationId: quickShorten parameters: - name: api_key in: path required: true description: Publisher API key (supports `nxl_live_...` or `nxl_quick_...`). schema: type: string - name: s in: query required: false description: "Target URL to shorten (alias: `url`)." schema: type: string format: uri - name: url in: query required: false description: Alternative parameter for target URL. schema: type: string format: uri - name: alias in: query required: false description: >- Publisher-chosen aliases are disabled. Passing one returns 422 alias_not_allowed. Leave it unset and a random code is generated: 4 case-sensitive base62 characters, growing a digit at a time up to 9 once every combination at a length is taken. schema: type: string - name: format in: query required: false description: Response format (`text` or `json`). Default is `text`. schema: type: string enum: [text, json] default: text - name: reuse in: query required: false description: Reuse existing active shortlink for identical target URL. Default is true. schema: type: boolean default: true - name: steps in: query required: false description: Required step gates (1-3). schema: type: integer minimum: 1 maximum: 3 default: 1 responses: '200': description: Successful link generation. headers: X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' content: text/plain: schema: type: string example: https://nxlnk.cc/aB3xQ application/json: schema: $ref: '#/components/schemas/LinkResponse' '400': description: Invalid target URL, empty input, or invalid alias. content: text/plain: schema: type: string example: "error: invalid_url" application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing, invalid, or revoked API key. content: text/plain: schema: type: string example: "error: unauthenticated" application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Insufficient scope, suspended account, or blocked target URL. content: text/plain: schema: type: string example: "error: url_blocked" application/json: schema: $ref: '#/components/schemas/ErrorResponse' '409': description: Requested custom alias is already registered. content: text/plain: schema: type: string example: "error: alias_taken" application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: Target URL points to a NexoLink system domain. content: text/plain: schema: type: string example: "error: self_reference" application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': $ref: '#/components/responses/RateLimited' # --------------------------------------------------------------------------- # REST API v1 Links # --------------------------------------------------------------------------- /api/v1/links: post: summary: Create shortlink description: >- Generate a new shortened URL with an optional title, gate step count and password. Custom aliases and expiry are not available: passing `alias` returns 422 alias_not_allowed and `expires_at` returns 422 expiry_not_supported. Shortlinks are permanent -- delete one to stop it resolving. operationId: createLink security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] requestBody: required: true content: application/json: schema: type: object required: - url properties: url: type: string format: uri description: Destination URL (HTTP/HTTPS, <= 4000 characters). example: https://contoh.com/download/volume-12.zip alias: type: string description: >- Custom code. Disabled by default: sending one returns 422 alias_not_allowed. Leave unset and a random code is generated. example: vol12 title: type: string description: Optional human-friendly label. example: Volume 12 Release steps: type: integer minimum: 1 maximum: 3 default: 1 description: Number of verification step pages (1-3). password: type: string nullable: true description: Optional password required before visitor can progress. reuse: type: boolean default: true description: When true and no custom alias/password is given, returns existing active link for this user. responses: '201': description: Shortlink successfully created or retrieved. headers: X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' content: application/json: schema: $ref: '#/components/schemas/LinkResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' get: summary: List shortlinks description: Retrieve a paginated list of links created by the authenticated publisher. operationId: listLinks security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] parameters: - name: page in: query required: false schema: type: integer default: 1 - name: per_page in: query required: false schema: type: integer default: 25 maximum: 100 - name: q in: query required: false description: Filter links by title or target URL search query. schema: type: string - name: status in: query required: false schema: type: string enum: [active, disabled, flagged] - name: sort in: query required: false schema: type: string enum: [created_at, views] default: created_at - name: order in: query required: false schema: type: string enum: [asc, desc] default: desc responses: '200': description: Paginated links. headers: X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' content: application/json: schema: type: object required: [ok, data, meta] properties: ok: type: boolean example: true data: type: array items: $ref: '#/components/schemas/LinkItem' meta: $ref: '#/components/schemas/PaginationMeta' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/RateLimited' /api/v1/links/bulk: post: summary: Create shortlinks in bulk description: Shorten up to 100 URLs in a single call. Rate limiting costs 1 request unit per 10 URLs. operationId: bulkCreateLinks security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] requestBody: required: true content: application/json: schema: type: object required: - urls properties: urls: type: array maxItems: 100 items: type: string format: uri example: - https://a.com/1 - https://b.com/2 steps: type: integer minimum: 1 maximum: 3 default: 1 responses: '200': description: Batch creation results per item. headers: X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' content: application/json: schema: type: object required: [ok, data] properties: ok: type: boolean example: true data: type: array items: type: object required: [url, ok] properties: url: type: string ok: type: boolean data: $ref: '#/components/schemas/LinkItem' error: $ref: '#/components/schemas/ErrorDetail' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/RateLimited' /api/v1/links/{code}: get: summary: Get link details description: Retrieve link status, metrics, and estimated USD earnings. operationId: getLink security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] parameters: - name: code in: path required: true schema: type: string responses: '200': description: Link details. headers: X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' content: application/json: schema: type: object required: [ok, data] properties: ok: type: boolean example: true data: $ref: '#/components/schemas/LinkItemDetail' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' patch: summary: Update link description: Update link title, steps, password, expiration, status, or target URL (only when views == 0). operationId: updateLink security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] parameters: - name: code in: path required: true schema: type: string requestBody: content: application/json: schema: type: object properties: title: type: string target_url: type: string format: uri steps: type: integer minimum: 1 maximum: 3 status: type: string enum: [active, disabled] password: type: string nullable: true responses: '200': description: Updated link details. content: application/json: schema: type: object required: [ok, data] properties: ok: type: boolean example: true data: $ref: '#/components/schemas/LinkItem' '403': description: target_url is immutable once the link has received views. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': $ref: '#/components/responses/NotFound' delete: summary: Soft delete link description: Deactivates link (status = disabled). Short code remains reserved to prevent hijacking. operationId: deleteLink security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] parameters: - name: code in: path required: true schema: type: string responses: '204': description: Link deactivated successfully. '404': $ref: '#/components/responses/NotFound' /api/v1/links/{code}/stats: get: summary: Link traffic statistics description: Aggregate views, valid monetized views, and earnings grouped by day or country. operationId: getLinkStats security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] parameters: - name: code in: path required: true schema: type: string - name: from in: query required: false schema: type: string format: date example: '2026-09-01' - name: to in: query required: false schema: type: string format: date example: '2026-09-13' - name: group_by in: query required: false schema: type: string enum: [day, country, referrer, device] default: day responses: '200': description: Link analytics breakdown. content: application/json: schema: $ref: '#/components/schemas/AnalyticsResponse' # --------------------------------------------------------------------------- # Account & Profile # --------------------------------------------------------------------------- /api/v1/account: get: summary: Get publisher account info description: Retrieve email, account status, current balance, min payout threshold, and referral link. operationId: getAccount security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] responses: '200': description: Account information. content: application/json: schema: type: object required: [ok, data] properties: ok: type: boolean example: true data: type: object required: [email, status, balance_usd, min_payout_usd, referral_url] properties: email: type: string format: email status: type: string example: active balance_usd: type: number format: float example: 45.20 min_payout_usd: type: number format: float example: 5.00 referral_url: type: string example: https://nexolink.cc/ref/NXREF123 /api/v1/account/stats: get: summary: Account-wide earnings and views description: Aggregated metrics across all links owned by publisher. operationId: getAccountStats security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] parameters: - name: from in: query schema: type: string format: date - name: to in: query schema: type: string format: date - name: group_by in: query schema: type: string enum: [day, country, referrer, device] default: day responses: '200': description: Account metrics. content: application/json: schema: type: object required: [ok, data] properties: ok: type: boolean example: true data: allOf: - $ref: '#/components/schemas/AnalyticsData' - type: object properties: balance_usd: type: number pending_payout_usd: type: number /api/v1/aliases/{alias}/availability: get: summary: Check custom alias availability description: Validates syntax, reserved words, and check if alias is taken. operationId: checkAliasAvailability security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] parameters: - name: alias in: path required: true schema: type: string responses: '200': description: Check result. content: application/json: schema: type: object required: [available] properties: available: type: boolean # --------------------------------------------------------------------------- # Full-Page Script API # --------------------------------------------------------------------------- /api/v1/fp/shorten: post: summary: Full-Page script batch URL converter description: Batch endpoint called by `fp.js` from publisher websites. Validates registered origin against public API key. operationId: fullPageShorten security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] requestBody: required: true content: application/json: schema: type: object required: [urls] properties: origin: type: string description: Origin host of publisher website. urls: type: array items: type: string format: uri responses: '200': description: Mapping of original URLs to shortened URLs. content: application/json: schema: type: object required: [ok, data] properties: ok: type: boolean example: true data: type: object additionalProperties: type: string # --------------------------------------------------------------------------- # API Key Management # --------------------------------------------------------------------------- # Reading payout history needs payouts:read. Requesting one needs # payouts:write, which is not granted by default: the request names the # destination account, so it is not something every key should be able to do. /api/v1/payouts: get: summary: List payout requests description: Payout requests raised by the authenticated publisher, newest first. operationId: listPayouts security: - bearerAuth: [] tags: [Payouts] responses: "200": description: Payout history. content: application/json: schema: type: object properties: ok: { type: boolean, example: true } data: type: array items: { $ref: "#/components/schemas/PayoutRequest" } "401": $ref: "#/components/responses/Unauthorized" post: summary: Request a payout description: >- Raise a payout request against the available balance. Send either `amount_usd` or `amount_micros`; sending neither requests the full available balance. The amount must clear the minimum for the chosen method, which /api/v1/payouts/methods reports. operationId: createPayout security: - bearerAuth: [] tags: [Payouts] requestBody: required: true content: application/json: schema: type: object properties: method: type: string description: One of the keys returned by /api/v1/payouts/methods. example: bank_transfer amount_usd: type: number format: float example: 25.5 amount_micros: type: integer description: Exact amount in micro-USD. Takes precedence over amount_usd. example: 25500000 responses: "201": description: Payout request created, pending review. content: application/json: schema: type: object properties: ok: { type: boolean, example: true } data: { $ref: "#/components/schemas/PayoutRequest" } "401": $ref: "#/components/responses/Unauthorized" "422": description: Amount below the method minimum, above the available balance, or the account is on fraud hold. /api/v1/payouts/methods: get: summary: List payout methods and minimums description: >- Supported methods with their minimum amounts, in USD and in IDR at the current exchange rate. operationId: listPayoutMethods security: - bearerAuth: [] tags: [Payouts] responses: "200": description: Methods and minimums. content: application/json: schema: type: object properties: ok: { type: boolean, example: true } data: type: array items: type: object properties: method: { type: string, example: bank_transfer } minimum_usd: { type: string, example: "3.00" } minimum_idr: { type: integer, example: 48000 } "401": $ref: "#/components/responses/Unauthorized" /api/v1/api-keys: get: summary: List API keys description: >- Keys belonging to the authenticated publisher. Only the prefix is ever returned; the secret is shown once, when the key is created. operationId: listApiKeys security: - bearerAuth: [] tags: [API Keys] responses: "200": description: The publisher's keys. content: application/json: schema: type: object properties: ok: { type: boolean, example: true } data: type: array items: { $ref: "#/components/schemas/ApiKey" } "401": $ref: "#/components/responses/Unauthorized" post: summary: Issue an API key description: >- Creates a key and returns its secret. The secret is not retrievable afterwards -- only a hash is stored -- so record it now or issue another. operationId: createApiKey security: - bearerAuth: [] tags: [API Keys] requestBody: required: false content: application/json: schema: type: object properties: label: type: string description: Your own name for the key, shown in the dashboard. example: WordPress plugin responses: "201": description: Key issued. The plaintext secret appears only in this response. content: application/json: schema: type: object properties: ok: { type: boolean, example: true } data: type: object properties: api_key: { $ref: "#/components/schemas/ApiKey" } plain_key: type: string description: Shown once and never again. example: nxl_live_9f2c... "401": $ref: "#/components/responses/Unauthorized" /api/v1/api-keys/{apiKey}: delete: summary: Revoke an API key description: Revokes a key immediately. Requests using it then fail with 401. operationId: revokeApiKey security: - bearerAuth: [] tags: [API Keys] parameters: - name: apiKey in: path required: true description: Key id, as returned by /api/v1/api-keys. schema: type: integer responses: "200": description: Key revoked. "401": $ref: "#/components/responses/Unauthorized" "404": description: No such key, or it belongs to another publisher. /api/v1/me: get: summary: Verify API key and publisher role operationId: getMe security: - BearerAuth: [] - ApiKeyHeader: [] - ApiKeyQuery: [] responses: '200': description: Authenticated identity. content: application/json: schema: type: object required: [ok, data] properties: ok: type: boolean data: type: object properties: user: type: object properties: id: type: integer email: type: string role: type: string status: type: string api_key: type: object properties: id: type: integer prefix: type: string kind: type: string scopes: type: array items: type: string components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: nxl_live_... description: "Standard API Secret Key passed via `Authorization: Bearer `." ApiKeyHeader: type: apiKey in: header name: X-API-Key description: Pass API key via `X-API-Key` header. ApiKeyQuery: type: apiKey in: query name: api description: Pass API key via `?api=` query parameter. headers: X-RateLimit-Limit: description: Maximum permitted requests within window (default 120/min). schema: type: integer example: 120 X-RateLimit-Remaining: description: Remaining request capacity in current window. schema: type: integer example: 117 X-RateLimit-Reset: description: UNIX timestamp when rate counter resets. schema: type: integer example: 1757740860 responses: BadRequest: description: Malformed input, invalid parameters, or validation error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Unauthorized: description: Missing, invalid, expired, or revoked API key. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Forbidden: description: Insufficient key scopes, suspended account, or blocked target URL. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' NotFound: description: Resource not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Conflict: description: Custom alias already taken. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Unprocessable: description: Target URL points to NexoLink internal domain. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' RateLimited: description: Rate limit exceeded (429 Too Many Requests). headers: Retry-After: schema: type: integer example: 15 X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' schemas: LinkItem: type: object required: [id, code, short_url, target_url, steps, status, total_views, total_valid_views, created_at] properties: id: type: string example: lnk_019943fe00117abc code: type: string example: vol12 short_url: type: string example: https://nxlnk.cc/vol12 target_url: type: string example: https://contoh.com/download/volume-12.zip title: type: string nullable: true example: Volume 12 steps: type: integer example: 1 status: type: string example: active total_views: type: integer example: 150 total_valid_views: type: integer example: 125 expires_at: type: string format: date-time nullable: true description: Always null. Shortlinks are permanent; delete a link to stop it resolving. created_at: type: string format: date-time LinkItemDetail: allOf: - $ref: '#/components/schemas/LinkItem' - type: object properties: earnings_usd: type: number format: float example: 3.41 LinkResponse: type: object required: [ok, data] properties: ok: type: boolean example: true data: allOf: - $ref: '#/components/schemas/LinkItem' - type: object properties: reused: type: boolean example: false PaginationMeta: type: object required: [page, per_page, total] properties: page: type: integer example: 1 per_page: type: integer example: 25 total: type: integer example: 312 AnalyticsResponse: type: object required: [ok, data] properties: ok: type: boolean example: true data: $ref: '#/components/schemas/AnalyticsData' AnalyticsData: type: object required: [range, totals, rows] properties: range: type: object required: [from, to] properties: from: type: string format: date to: type: string format: date totals: type: object required: [views, valid_views, earnings_usd] properties: views: type: integer valid_views: type: integer earnings_usd: type: number format: float rows: type: array items: type: object properties: date: type: string format: date country: type: string example: ID views: type: integer valid_views: type: integer earnings_usd: type: number format: float PayoutRequest: type: object description: One payout request. Transfers are made by hand; an administrator marks the outcome. properties: id: type: integer example: 41 amount_usd: type: number format: float example: 25.5 amount_micros: type: integer description: The authoritative amount. amount_usd is this rounded for display. example: 25500000 amount_idr: type: integer description: Converted at the exchange rate held in kv_store at the time of the request. example: 408000 method: type: string description: The payout method as it stood when the request was raised, kept even if the account's method changes later. example: bank_transfer status: type: string enum: [pending, approved, paid, cancelled] example: pending tx_ref: type: string nullable: true description: Transfer reference, filled in once paid. admin_note: type: string nullable: true requested_at: type: string format: date-time processed_at: type: string format: date-time nullable: true ApiKey: type: object description: An API key. The secret itself is stored only as a hash and is never returned after creation. properties: id: type: integer example: 7 key_prefix: type: string description: The visible leading portion, enough to tell keys apart. example: nxl_live_9f2c kind: type: string enum: [live, quick] example: live label: type: string nullable: true example: WordPress plugin scopes: type: array items: type: string example: [links:read, links:write, stats:read, payouts:read] allowed_origins: type: string nullable: true description: Comma-separated origins permitted to use this key from a browser. last_used_at: type: string format: date-time nullable: true revoked_at: type: string format: date-time nullable: true created_at: type: string format: date-time is_active: type: boolean example: true ErrorResponse: type: object required: [ok, error] properties: ok: type: boolean example: false error: $ref: '#/components/schemas/ErrorDetail' ErrorDetail: type: object required: [code, message] properties: code: type: string example: invalid_url message: type: string example: Only HTTP and HTTPS URLs are supported. field: type: string nullable: true example: url