openapi: 3.1.0 info: title: Plinkly API version: "2.0.0" description: | Developer API for Plinkly (Link Shortener & Analytics). Base URL: `https://plink.ly/apiv` Authentication (Public API): - Use `Authorization: Bearer `. - Tokens are expected to be 64 hex characters. Notes: - Most endpoints return JSON. - The API applies rate limiting (see responses for HTTP 429). servers: - url: https://plink.ly/apiv tags: - name: Public description: Bearer-token endpoints intended for developer integrations. - name: Integrations description: Server-to-server integration endpoints (may require shared secrets). - name: Internal description: Session-based endpoints used by the Plinkly web app. components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: "token" hmacSignature: type: apiKey in: header name: X-G2PICK-SIGNATURE description: | HMAC SHA-256 of `${timestamp}${rawBody}` using the shared secret (`PLINKLY_API_SECRET`). Used with `X-G2PICK-TIMESTAMP`. parameters: SlugQuery: in: query name: slug required: true schema: type: string description: Short code/slug. responses: Error400: description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error' Error401: description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error' Error403: description: Forbidden content: application/json: schema: $ref: '#/components/schemas/Error' Error404: description: Not found content: application/json: schema: $ref: '#/components/schemas/Error' Error405: description: Method not allowed content: application/json: schema: $ref: '#/components/schemas/Error' Error422: description: Unprocessable entity content: application/json: schema: $ref: '#/components/schemas/IntegrationError' Error429: description: Rate limit exceeded content: application/json: schema: $ref: '#/components/schemas/Error' schemas: Error: type: object additionalProperties: false properties: error: type: string required: [error] examples: missing_bearer: value: { error: "Missing Bearer token" } invalid_token: value: { error: "Invalid token" } ShortenRequest: type: object additionalProperties: false properties: url: type: string format: uri description: Destination URL. slug: type: string nullable: true description: Optional custom slug (validated server-side). expire_days: type: integer nullable: true description: | Expiration in days. `0` means no expiry (unlimited). Negative values are rejected. max_clicks: type: integer nullable: true description: | Maximum clicks allowed. `0` means no limit (unlimited). Negative values are rejected. domain_id: type: integer nullable: true description: | Optional custom domain id. `0` means default domain. required: [url] ShortLink: type: object properties: slug: type: string short_url: type: string format: uri original_url: type: string format: uri nullable: true expires_at: type: string format: date-time nullable: true max_clicks: type: integer nullable: true domain_id: type: integer nullable: true required: [slug] examples: created: value: slug: "abc12" short_url: "https://plink.ly/abc12" original_url: "https://example.com" expires_at: null max_clicks: null domain_id: null LinkStats: allOf: - $ref: '#/components/schemas/ShortLink' - type: object properties: id: type: integer clicks: type: integer created_at: type: string format: date-time last7: type: object description: Daily clicks (last 7 days), keyed by date (YYYY-MM-DD). additionalProperties: type: integer DeleteResponse: type: object additionalProperties: false properties: status: type: string enum: [deleted] required: [status] VisitorLinksResponse: type: object properties: links: type: array items: type: object properties: slug: type: string short_url: type: string format: uri required: [slug, short_url] required: [links] ActivateLinkShorterRequest: type: object description: | Signed server-to-server payload. - `plan=lifetime` requires `order_id`. - Other plans require `subscription_id` + `expires_at`. properties: user_email: type: string format: email plan: type: string description: "Examples: lifetime, pro, unlimited" provider: type: string default: wordpress order_id: type: string nullable: true amount: type: number nullable: true currency: type: string nullable: true purchased_at: type: string nullable: true description: "MySQL datetime string. If absent, server uses current time." subscription_id: type: string nullable: true expires_at: type: string nullable: true description: "MySQL datetime string: YYYY-MM-DD HH:MM:SS" required: [user_email, plan] ActivateLinkShorterSuccess: type: object properties: status: type: string enum: [success] type: type: string enum: [lifetime, subscription] user_id: type: integer plan_id: type: integer nullable: true order_id: type: string nullable: true subscription_id: type: string nullable: true plan: type: string nullable: true expires_at: type: string nullable: true required: [status, type, user_id] IntegrationError: type: object properties: status: type: string enum: [error] message: type: string required: [status, message] security: - bearerAuth: [] paths: /shorten: post: tags: [Public] summary: Create a short link description: | Creates a short link for the authenticated user. Plan notes: - Free plan cannot use custom domains (`domain_id > 0`). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ShortenRequest' responses: '200': description: Created content: application/json: schema: $ref: '#/components/schemas/ShortLink' '400': { $ref: '#/components/responses/Error400' } '401': { $ref: '#/components/responses/Error401' } '403': { $ref: '#/components/responses/Error403' } '405': { $ref: '#/components/responses/Error405' } '429': { $ref: '#/components/responses/Error429' } /stats: get: tags: [Public] summary: Get link stats parameters: - $ref: '#/components/parameters/SlugQuery' responses: '200': description: Stats content: application/json: schema: $ref: '#/components/schemas/LinkStats' '400': { $ref: '#/components/responses/Error400' } '401': { $ref: '#/components/responses/Error401' } '403': { $ref: '#/components/responses/Error403' } '404': { $ref: '#/components/responses/Error404' } '429': { $ref: '#/components/responses/Error429' } /delete: delete: tags: [Public] summary: Delete a short link parameters: - $ref: '#/components/parameters/SlugQuery' responses: '200': description: Deleted content: application/json: schema: $ref: '#/components/schemas/DeleteResponse' '400': { $ref: '#/components/responses/Error400' } '401': { $ref: '#/components/responses/Error401' } '403': { $ref: '#/components/responses/Error403' } '404': { $ref: '#/components/responses/Error404' } '405': { $ref: '#/components/responses/Error405' } '429': { $ref: '#/components/responses/Error429' } /get_visitor_links: get: tags: [Internal] summary: Get last visitor-created links (cookie-based) description: | Used by Plinkly UI for anonymous visitors. Authentication: - Reads `visitor_token` from cookies. security: [] responses: '200': description: Visitor links content: application/json: schema: $ref: '#/components/schemas/VisitorLinksResponse' /activate-linkshorter: post: tags: [Integrations] summary: Activate LinkShorter plan for a user (signed request) description: | Server-to-server endpoint. Required headers: - `X-G2PICK-TIMESTAMP`: Unix timestamp. - `X-G2PICK-SIGNATURE`: HMAC SHA-256 of `${timestamp}${rawBody}`. The server rejects replayed requests outside a ±5 minute window. Note: The actual shared secret is configured server-side as `PLINKLY_API_SECRET`. security: - hmacSignature: [] parameters: - in: header name: X-G2PICK-TIMESTAMP required: true schema: type: integer description: Unix timestamp (seconds). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ActivateLinkShorterRequest' responses: '200': description: Activated content: application/json: schema: $ref: '#/components/schemas/ActivateLinkShorterSuccess' '401': description: Signature or auth failure content: application/json: schema: $ref: '#/components/schemas/IntegrationError' '408': description: Stale request (replay protection) content: application/json: schema: $ref: '#/components/schemas/IntegrationError' '422': { $ref: '#/components/responses/Error422' } '500': description: Server error content: application/json: schema: $ref: '#/components/schemas/IntegrationError' /billing/status: get: tags: [Internal] summary: Billing status for logged-in user (session-based) description: | Used by Plinkly web app after checkout. Requires a valid Plinkly session (cookie-based login). security: [] parameters: - in: query name: provider required: false schema: type: string enum: [stripe, paypal] default: stripe responses: '200': description: Billing status content: application/json: schema: type: object properties: ok: { type: boolean } provider: { type: string } subscription: nullable: true type: object properties: plan: { type: string } status: { type: string } started_at: { type: string, nullable: true } expires_at: { type: string, nullable: true } '401': description: Unauthorized content: application/json: schema: type: object properties: ok: { type: boolean } error: { type: string } /stripe/create_checkout_session: post: tags: [Internal] summary: Create a Stripe Checkout Session (session-based) description: | Used by the Plinkly pricing page. Requires logged-in user session. security: [] requestBody: required: true content: application/json: schema: type: object properties: plan: type: string enum: [pro, unlimited, lifetime] required: [plan] responses: '200': description: Checkout URL content: application/json: schema: type: object properties: ok: { type: boolean } url: { type: string, format: uri } '401': description: Unauthorized content: application/json: schema: type: object properties: ok: { type: boolean } error: { type: string } /stripe/webhook: post: tags: [Internal] summary: Stripe webhook handler description: | Stripe webhook handler. Authentication: - Validates `Stripe-Signature` using server-side `STRIPE_WEBHOOK_SECRET`. security: [] responses: '200': description: Acknowledged content: text/plain: schema: { type: string } '400': description: Invalid signature/payload content: text/plain: schema: { type: string } '500': description: Server error content: text/plain: schema: { type: string }