openapi: 3.0.0 info: version: 1.0.0 title: Zulk API description: URL shortener API for Zulk platform servers: - url: https://api.zu.lk description: Production server - url: http://localhost:8787 description: Development server tags: - name: Authentication description: 🔐 OAuth endpoints for web app authentication (no API key required) - name: API v1 - Users description: 👥 User management endpoints (requires API key) - name: API v1 - Organizations description: 🏢 Organization management endpoints (requires API key) - name: API v1 - Links description: 🔗 URL shortening endpoints (requires API key) - name: API v1 - Analytics description: 📊 Analytics and reporting endpoints (requires API key) components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-KEY description: API Key for authentication ApiSecretAuth: type: apiKey in: header name: X-API-Secret description: API Secret for authentication schemas: Link: type: object properties: id: type: string example: "1" description: Link ID key: type: string example: abc123 description: Short link key url: type: string format: uri example: https://example.com description: Original URL created_at: type: string format: date-time example: 2025-01-01T00:00:00Z description: Creation timestamp owner: type: string example: org123 description: Organization ID that owns this link CreateLink: type: object required: - url properties: key: type: string example: custom-key description: Custom short key (optional) url: type: string format: uri example: https://example.com description: URL to shorten length: type: integer minimum: 3 maximum: 10 example: 6 description: "Length of generated key (3-10, default: 6)" CreateLinkResponse: type: object properties: message: type: string example: Record added successfully created_at: type: string format: date-time example: 2025-01-01T00:00:00Z shortLink: type: string example: https://zu.lk/abc123 key: type: string example: abc123 url: type: string format: uri example: https://example.com length: type: integer example: 6 Error: type: object properties: error: type: string example: Error message description: Error message User: type: object properties: id: type: string example: "123" description: User ID name: type: string example: John Doe description: User name age: type: integer example: 42 description: User age AuthUser: type: object properties: email: type: string format: email example: user@example.com description: User email (lowercased) user: type: object properties: id: type: string example: "123456789" description: Google user ID name: type: string example: John Doe description: User display name email: type: string format: email example: user@example.com description: User email from Google picture: type: string format: uri example: https://lh3.googleusercontent.com/... description: User profile picture URL description: Google OAuth user data token: type: string example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... description: JWT token apiKey: type: string example: ak_1234567890abcdef description: User API key apiToken: type: string example: at_1234567890abcdef description: User API token Organization: type: object properties: id: type: number example: 1 description: Organization ID name: type: string example: Acme Corp description: Organization name role: type: string example: OWNER description: User role in the organization OrganizationMember: type: object properties: id: type: number example: 123 description: User ID name: type: string example: John Doe description: User name email: type: string format: email example: john@example.com description: User email role: type: string enum: - MANAGER - ADMIN - OWNER example: MANAGER description: User role in the organization AddMemberRequest: type: object required: - email properties: email: type: string format: email example: newmember@example.com description: Email of the user to add role: type: string enum: - MANAGER - ADMIN - OWNER example: MANAGER default: MANAGER description: Role to assign to the new member UpdateRoleRequest: type: object required: - role properties: role: type: string enum: - MANAGER - ADMIN - OWNER example: ADMIN description: New role for the member AnalyticsResponse: type: object properties: orgId: type: string example: "1" description: Organization ID analytics: type: object properties: totalClicks: type: number example: 1250 description: Total click count dateRange: type: object properties: from: type: string example: -7d description: Start date of the range to: type: string example: today description: End date of the range interval: type: string example: day description: Data interval data: type: array items: type: number example: - 120 - 150 - 89 - 200 - 180 - 145 - 220 description: Click data points labels: type: array items: type: string example: - 2025-08-09 - 2025-08-10 - 2025-08-11 - 2025-08-12 - 2025-08-13 - 2025-08-14 - 2025-08-15 description: Date labels for data points generatedAt: type: string format: date-time example: 2025-08-15T12:00:00Z description: Timestamp when analytics were generated UpdateLinkRequest: type: object required: - url - key properties: url: type: string format: uri example: https://updated-example.com description: Updated URL key: type: string example: updated-key description: Updated short key CreateOrganization: type: object required: - name properties: name: type: string example: Acme Corp description: Organization name security: - ApiKeyAuth: [] ApiSecretAuth: [] paths: /v1/organizations: get: summary: Get user organizations description: Retrieve organizations that the authenticated user has access to tags: - API v1 - Organizations security: - ApiKeyAuth: [] ApiSecretAuth: [] responses: "200": description: Successfully retrieved user organizations content: application/json: schema: type: array items: $ref: "#/components/schemas/Organization" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" post: summary: Create a new organization description: Create a new organization and assign the authenticated user as owner tags: - API v1 - Organizations security: - ApiKeyAuth: [] ApiSecretAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateOrganization" responses: "200": description: Successfully created organization content: application/json: schema: type: object properties: message: type: string example: Organization created successfully id: type: number example: 1 description: Organization ID name: type: string example: Acme Corp description: Organization name role: type: string example: OWNER description: User role in the organization "400": description: Bad request - Name is required content: application/json: schema: $ref: "#/components/schemas/Error" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal server error content: application/json: schema: $ref: "#/components/schemas/Error" /v1/organizations/{orgId}/members: get: summary: Get organization members description: Retrieve all members of a specific organization tags: - API v1 - Organizations security: - ApiKeyAuth: [] ApiSecretAuth: [] parameters: - name: orgId in: path required: true schema: type: string description: Organization ID example: "1" responses: "200": description: Successfully retrieved organization members content: application/json: schema: type: object properties: members: type: array items: $ref: "#/components/schemas/OrganizationMember" total: type: number example: 5 description: Total number of members "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Access denied to this organization content: application/json: schema: $ref: "#/components/schemas/Error" post: summary: Add member to organization description: Add a new member to an organization (requires ADMIN or OWNER role) tags: - API v1 - Organizations security: - ApiKeyAuth: [] ApiSecretAuth: [] parameters: - name: orgId in: path required: true schema: type: string description: Organization ID example: "1" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AddMemberRequest" responses: "200": description: Successfully added member to organization content: application/json: schema: type: object properties: message: type: string example: Member added successfully member: $ref: "#/components/schemas/OrganizationMember" "400": description: Bad request - Email is required or invalid role content: application/json: schema: $ref: "#/components/schemas/Error" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Access denied - Requires ADMIN or OWNER role content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: User not found - User must register first content: application/json: schema: $ref: "#/components/schemas/Error" example: error: User not found. User must register first. "409": description: Conflict - User is already a member content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal server error content: application/json: schema: $ref: "#/components/schemas/Error" /v1/organizations/{orgId}/members/{memberId}/role: patch: summary: Update member role description: Update the role of a specific member in an organization (requires ADMIN or OWNER role) tags: - API v1 - Organizations security: - ApiKeyAuth: [] ApiSecretAuth: [] parameters: - name: orgId in: path required: true schema: type: string description: Organization ID example: "1" - name: memberId in: path required: true schema: type: string description: Member ID example: "123" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateRoleRequest" responses: "200": description: Successfully updated member role content: application/json: schema: type: object properties: message: type: string example: Member role updated successfully member: type: object properties: id: type: number example: 123 name: type: string example: John Doe email: type: string example: john@example.com role: type: string example: ADMIN previousRole: type: string example: MANAGER "400": description: Bad request - Role is required or invalid content: application/json: schema: $ref: "#/components/schemas/Error" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Access denied - Requires ADMIN or OWNER role, or cannot change own role content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Member not found in this organization content: application/json: schema: $ref: "#/components/schemas/Error" example: error: Member not found in this organization "500": description: Internal server error content: application/json: schema: $ref: "#/components/schemas/Error" /v1/organizations/{orgId}/members/{memberId}: delete: summary: Remove member from organization description: Remove a member from an organization (requires ADMIN or OWNER role). Cannot remove yourself or the current OWNER. tags: - API v1 - Organizations security: - ApiKeyAuth: [] ApiSecretAuth: [] parameters: - name: orgId in: path required: true schema: type: string description: Organization ID example: "1" - name: memberId in: path required: true schema: type: string description: Member ID example: "123" responses: "200": description: Successfully removed member from organization content: application/json: schema: type: object properties: message: type: string example: Member removed successfully removedMember: $ref: "#/components/schemas/OrganizationMember" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Access denied - Requires ADMIN or OWNER role, cannot remove yourself, or cannot remove last OWNER content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Member not found in this organization content: application/json: schema: $ref: "#/components/schemas/Error" example: error: Member not found in this organization "500": description: Internal server error content: application/json: schema: $ref: "#/components/schemas/Error" /v1/organizations/{orgId}/analytics/clicks: get: summary: Get organization click analytics description: Retrieve click analytics data for an organization's links from PostHog tags: - API v1 - Analytics security: - ApiKeyAuth: [] ApiSecretAuth: [] parameters: - name: orgId in: path required: true schema: type: string description: Organization ID example: "1" - name: date_from in: query required: false schema: type: string description: "Start date for analytics (default: -7d)" example: -7d - name: date_to in: query required: false schema: type: string description: "End date for analytics (default: today)" example: today - name: interval in: query required: false schema: type: string description: "Data interval (default: day)" example: day responses: "200": description: Successfully retrieved analytics data content: application/json: schema: $ref: "#/components/schemas/AnalyticsResponse" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Access denied to this organization content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal server error - Failed to fetch analytics data content: application/json: schema: $ref: "#/components/schemas/Error" /v1/organizations/{orgId}/links: get: summary: Get organization links description: Retrieve all links for a specific organization tags: - API v1 - Links security: - ApiKeyAuth: [] ApiSecretAuth: [] parameters: - name: orgId in: path required: true schema: type: string description: Organization ID example: "1" responses: "200": description: Successfully retrieved organization links content: application/json: schema: type: array items: $ref: "#/components/schemas/Link" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Access denied to this organization content: application/json: schema: $ref: "#/components/schemas/Error" post: summary: Create a new link in organization description: Create a new short link for the given URL in the specified organization tags: - API v1 - Links security: - ApiKeyAuth: [] ApiSecretAuth: [] parameters: - name: orgId in: path required: true schema: type: string description: Organization ID example: "1" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateLink" responses: "200": description: Successfully created short link content: application/json: schema: $ref: "#/components/schemas/CreateLinkResponse" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Access denied to this organization content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal server error content: application/json: schema: $ref: "#/components/schemas/Error" /v1/organizations/{orgId}/links/{id}: get: summary: Get a specific link from organization description: Retrieve a specific link by ID from the specified organization tags: - API v1 - Links security: - ApiKeyAuth: [] ApiSecretAuth: [] parameters: - name: orgId in: path required: true schema: type: string description: Organization ID example: "1" - name: id in: path required: true schema: type: string description: Link ID example: "1" responses: "200": description: Successfully retrieved link content: application/json: schema: $ref: "#/components/schemas/Link" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Access denied to this organization content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Link not found content: application/json: schema: $ref: "#/components/schemas/Error" example: error: Link not found /v1/organizations/{orgId}/links/{linkId}: put: summary: Update a link in organization description: Update an existing short link for the specified organization tags: - API v1 - Links security: - ApiKeyAuth: [] ApiSecretAuth: [] parameters: - name: orgId in: path required: true schema: type: string description: Organization ID example: "1" - name: linkId in: path required: true schema: type: string description: Link ID example: "1" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateLinkRequest" responses: "200": description: Successfully updated link content: application/json: schema: type: object properties: message: type: string example: Link updated successfully id: type: string example: "1" description: Link ID key: type: string example: updated-key description: Updated short key url: type: string format: uri example: https://updated-example.com description: Updated URL shortLink: type: string example: https://zu.lk/updated-key description: Full short link URL updated_at: type: string format: date-time example: 2025-08-15T12:00:00Z description: Update timestamp previousKey: type: string example: old-key description: Previous short key "400": description: Bad request - URL and key are required content: application/json: schema: $ref: "#/components/schemas/Error" "401": description: Unauthorized - Invalid API credentials content: application/json: schema: $ref: "#/components/schemas/Error" "403": description: Access denied to this organization content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Link not found in this organization content: application/json: schema: $ref: "#/components/schemas/Error" example: error: Link not found in this organization "409": description: Conflict - Key is already taken content: application/json: schema: $ref: "#/components/schemas/Error" "500": description: Internal server error content: application/json: schema: $ref: "#/components/schemas/Error"