Milindu Kumarage commited on
Commit
393823d
·
1 Parent(s): 73b50af
Files changed (2) hide show
  1. main.py +206 -0
  2. zulk-api.yaml +984 -0
main.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+
5
+ # Zu.lk API Base URL
6
+ API_BASE_URL = "https://api.zu.lk"
7
+
8
+ def parse_auth_header(auth_header):
9
+ """Parse authorization header to extract API key and secret"""
10
+ if not auth_header or auth_header.strip() == "":
11
+ return None, None
12
+
13
+ # Format: "Bearer OfNJsl3lmt:$2b$10$qf2ZMINlvw"
14
+ try:
15
+ # Split by space and get the second part (after "Bearer ")
16
+ parts = auth_header.strip().split(" ", 1)
17
+ if len(parts) != 2 or parts[0] != "Bearer":
18
+ return None, None
19
+
20
+ # Split the credentials by colon
21
+ credentials = parts[1]
22
+ api_key, api_secret = credentials.split(":", 1)
23
+ return api_key, api_secret
24
+ except ValueError:
25
+ return None, None
26
+
27
+ def make_api_request(endpoint, method="GET", data=None, api_key=None, api_secret=None):
28
+ """Make authenticated request to Zu.lk API"""
29
+ if not api_key or not api_secret:
30
+ return {"error": "Invalid authentication credentials"}
31
+
32
+ headers = {
33
+ "X-API-KEY": api_key,
34
+ "X-API-Secret": api_secret,
35
+ "Content-Type": "application/json"
36
+ }
37
+
38
+ url = f"{API_BASE_URL}{endpoint}"
39
+
40
+ try:
41
+ if method == "GET":
42
+ response = requests.get(url, headers=headers)
43
+ elif method == "POST":
44
+ response = requests.post(url, headers=headers, json=data)
45
+ elif method == "PUT":
46
+ response = requests.put(url, headers=headers, json=data)
47
+ elif method == "DELETE":
48
+ response = requests.delete(url, headers=headers)
49
+ else:
50
+ return {"error": f"Unsupported method: {method}"}
51
+
52
+ return response.json()
53
+ except requests.exceptions.RequestException as e:
54
+ return {"error": f"Request failed: {str(e)}"}
55
+ except json.JSONDecodeError:
56
+ return {"error": "Invalid JSON response"}
57
+
58
+ def get_organizations(request: gr.Request):
59
+ """Get user organizations"""
60
+ api_key, api_secret = parse_auth_header(request.headers.get('authorization', ''))
61
+
62
+ if not api_key or not api_secret:
63
+ return "Error: Authorization is required (format: Bearer api_key:api_secret)"
64
+
65
+ result = make_api_request("/v1/organizations", api_key=api_key, api_secret=api_secret)
66
+ return json.dumps(result, indent=2)
67
+
68
+ def create_link(org_id, url, custom_key, length, request: gr.Request):
69
+ """Create a new short link"""
70
+ api_key, api_secret = parse_auth_header(request.headers.get('authorization', ''))
71
+
72
+ if not api_key or not api_secret:
73
+ return "Error: Authorization is required (format: Bearer api_key:api_secret)"
74
+
75
+ if not org_id or not url:
76
+ return "Error: Organization ID and URL are required"
77
+
78
+ data = {"url": url}
79
+ if custom_key:
80
+ data["key"] = custom_key
81
+ if length and length > 0:
82
+ data["length"] = length
83
+
84
+ result = make_api_request(f"/v1/organizations/{org_id}/links", method="POST",
85
+ data=data, api_key=api_key, api_secret=api_secret)
86
+ return json.dumps(result, indent=2)
87
+
88
+ def get_org_links(org_id, request: gr.Request):
89
+ """Get all links for an organization"""
90
+ api_key, api_secret = parse_auth_header(request.headers.get('authorization', ''))
91
+
92
+ if not api_key or not api_secret:
93
+ return "Error: Authorization is required (format: Bearer api_key:api_secret)"
94
+
95
+ if not org_id:
96
+ return "Error: Organization ID is required"
97
+
98
+ result = make_api_request(f"/v1/organizations/{org_id}/links",
99
+ api_key=api_key, api_secret=api_secret)
100
+ return json.dumps(result, indent=2)
101
+
102
+ def get_analytics(org_id, date_from, date_to, interval, request: gr.Request):
103
+ """Get organization analytics"""
104
+ api_key, api_secret = parse_auth_header(request.headers.get('authorization', ''))
105
+
106
+ if not api_key or not api_secret:
107
+ return "Error: Authorization is required (format: Bearer api_key:api_secret)"
108
+
109
+ if not org_id:
110
+ return "Error: Organization ID is required"
111
+
112
+ params = []
113
+ if date_from:
114
+ params.append(f"date_from={date_from}")
115
+ if date_to:
116
+ params.append(f"date_to={date_to}")
117
+ if interval:
118
+ params.append(f"interval={interval}")
119
+
120
+ query_string = "&".join(params)
121
+ endpoint = f"/v1/organizations/{org_id}/analytics/clicks"
122
+ if query_string:
123
+ endpoint += f"?{query_string}"
124
+
125
+ result = make_api_request(endpoint, api_key=api_key, api_secret=api_secret)
126
+ return json.dumps(result, indent=2)
127
+
128
+ def update_link(org_id, link_id, new_url, new_key, request: gr.Request):
129
+ """Update an existing link"""
130
+ api_key, api_secret = parse_auth_header(request.headers.get('authorization', ''))
131
+
132
+ if not api_key or not api_secret:
133
+ return "Error: Authorization is required (format: Bearer api_key:api_secret)"
134
+
135
+ if not org_id or not link_id or not new_url or not new_key:
136
+ return "Error: Organization ID, Link ID, URL, and Key are required"
137
+
138
+ data = {"url": new_url, "key": new_key}
139
+
140
+ result = make_api_request(f"/v1/organizations/{org_id}/links/{link_id}",
141
+ method="PUT", data=data, api_key=api_key, api_secret=api_secret)
142
+ return json.dumps(result, indent=2)
143
+
144
+ # Create Gradio interface with tabs
145
+ with gr.Blocks(title="Zu.lk API Client") as demo:
146
+ gr.Markdown("# Zu.lk API Client")
147
+ gr.Markdown("Authorization format: `Bearer api_key:api_secret`")
148
+
149
+ with gr.Tab("Organizations"):
150
+ with gr.Row():
151
+ org_btn = gr.Button("Get My Organizations")
152
+ org_output = gr.Textbox(label="Organizations", lines=10)
153
+
154
+ org_btn.click(get_organizations, outputs=org_output)
155
+
156
+ with gr.Tab("Create Link"):
157
+ with gr.Column():
158
+ create_org_id = gr.Textbox(label="Organization ID", placeholder="1")
159
+ create_url = gr.Textbox(label="URL to Shorten", placeholder="https://example.com")
160
+ create_key = gr.Textbox(label="Custom Key (optional)", placeholder="my-link")
161
+ create_length = gr.Number(label="Key Length (3-10)", value=6, minimum=3, maximum=10)
162
+ create_btn = gr.Button("Create Link")
163
+ create_output = gr.Textbox(label="Result", lines=8)
164
+
165
+ create_btn.click(create_link,
166
+ inputs=[create_org_id, create_url, create_key, create_length],
167
+ outputs=create_output)
168
+
169
+ with gr.Tab("Manage Links"):
170
+ with gr.Column():
171
+ links_org_id = gr.Textbox(label="Organization ID", placeholder="1")
172
+ get_links_btn = gr.Button("Get All Links")
173
+ links_output = gr.Textbox(label="Links", lines=10)
174
+
175
+ get_links_btn.click(get_org_links,
176
+ inputs=[links_org_id],
177
+ outputs=links_output)
178
+
179
+ with gr.Tab("Update Link"):
180
+ with gr.Column():
181
+ update_org_id = gr.Textbox(label="Organization ID", placeholder="1")
182
+ update_link_id = gr.Textbox(label="Link ID", placeholder="1")
183
+ update_url = gr.Textbox(label="New URL", placeholder="https://updated-example.com")
184
+ update_key = gr.Textbox(label="New Key", placeholder="updated-key")
185
+ update_btn = gr.Button("Update Link")
186
+ update_output = gr.Textbox(label="Result", lines=8)
187
+
188
+ update_btn.click(update_link,
189
+ inputs=[update_org_id, update_link_id, update_url, update_key],
190
+ outputs=update_output)
191
+
192
+ with gr.Tab("Analytics"):
193
+ with gr.Column():
194
+ analytics_org_id = gr.Textbox(label="Organization ID", placeholder="1")
195
+ analytics_from = gr.Textbox(label="Date From (optional)", placeholder="-7d")
196
+ analytics_to = gr.Textbox(label="Date To (optional)", placeholder="today")
197
+ analytics_interval = gr.Textbox(label="Interval (optional)", placeholder="day")
198
+ analytics_btn = gr.Button("Get Analytics")
199
+ analytics_output = gr.Textbox(label="Analytics", lines=12)
200
+
201
+ analytics_btn.click(get_analytics,
202
+ inputs=[analytics_org_id, analytics_from, analytics_to, analytics_interval],
203
+ outputs=analytics_output)
204
+
205
+ if __name__ == "__main__":
206
+ demo.launch(mcp_server=True, share=True)
zulk-api.yaml ADDED
@@ -0,0 +1,984 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ openapi: 3.0.0
2
+ info:
3
+ version: 1.0.0
4
+ title: Zulk API
5
+ description: URL shortener API for Zulk platform
6
+ servers:
7
+ - url: https://api.zu.lk
8
+ description: Production server
9
+ - url: http://localhost:8787
10
+ description: Development server
11
+ tags:
12
+ - name: Authentication
13
+ description: 🔐 OAuth endpoints for web app authentication (no API key required)
14
+ - name: API v1 - Users
15
+ description: 👥 User management endpoints (requires API key)
16
+ - name: API v1 - Organizations
17
+ description: 🏢 Organization management endpoints (requires API key)
18
+ - name: API v1 - Links
19
+ description: 🔗 URL shortening endpoints (requires API key)
20
+ - name: API v1 - Analytics
21
+ description: 📊 Analytics and reporting endpoints (requires API key)
22
+ components:
23
+ securitySchemes:
24
+ ApiKeyAuth:
25
+ type: apiKey
26
+ in: header
27
+ name: X-API-KEY
28
+ description: API Key for authentication
29
+ ApiSecretAuth:
30
+ type: apiKey
31
+ in: header
32
+ name: X-API-Secret
33
+ description: API Secret for authentication
34
+ schemas:
35
+ Link:
36
+ type: object
37
+ properties:
38
+ id:
39
+ type: string
40
+ example: "1"
41
+ description: Link ID
42
+ key:
43
+ type: string
44
+ example: abc123
45
+ description: Short link key
46
+ url:
47
+ type: string
48
+ format: uri
49
+ example: https://example.com
50
+ description: Original URL
51
+ created_at:
52
+ type: string
53
+ format: date-time
54
+ example: 2025-01-01T00:00:00Z
55
+ description: Creation timestamp
56
+ owner:
57
+ type: string
58
+ example: org123
59
+ description: Organization ID that owns this link
60
+ CreateLink:
61
+ type: object
62
+ required:
63
+ - url
64
+ properties:
65
+ key:
66
+ type: string
67
+ example: custom-key
68
+ description: Custom short key (optional)
69
+ url:
70
+ type: string
71
+ format: uri
72
+ example: https://example.com
73
+ description: URL to shorten
74
+ length:
75
+ type: integer
76
+ minimum: 3
77
+ maximum: 10
78
+ example: 6
79
+ description: "Length of generated key (3-10, default: 6)"
80
+ CreateLinkResponse:
81
+ type: object
82
+ properties:
83
+ message:
84
+ type: string
85
+ example: Record added successfully
86
+ created_at:
87
+ type: string
88
+ format: date-time
89
+ example: 2025-01-01T00:00:00Z
90
+ shortLink:
91
+ type: string
92
+ example: https://zu.lk/abc123
93
+ key:
94
+ type: string
95
+ example: abc123
96
+ url:
97
+ type: string
98
+ format: uri
99
+ example: https://example.com
100
+ length:
101
+ type: integer
102
+ example: 6
103
+ Error:
104
+ type: object
105
+ properties:
106
+ error:
107
+ type: string
108
+ example: Error message
109
+ description: Error message
110
+ User:
111
+ type: object
112
+ properties:
113
+ id:
114
+ type: string
115
+ example: "123"
116
+ description: User ID
117
+ name:
118
+ type: string
119
+ example: John Doe
120
+ description: User name
121
+ age:
122
+ type: integer
123
+ example: 42
124
+ description: User age
125
+ AuthUser:
126
+ type: object
127
+ properties:
128
+ email:
129
+ type: string
130
+ format: email
131
+ example: user@example.com
132
+ description: User email (lowercased)
133
+ user:
134
+ type: object
135
+ properties:
136
+ id:
137
+ type: string
138
+ example: "123456789"
139
+ description: Google user ID
140
+ name:
141
+ type: string
142
+ example: John Doe
143
+ description: User display name
144
+ email:
145
+ type: string
146
+ format: email
147
+ example: user@example.com
148
+ description: User email from Google
149
+ picture:
150
+ type: string
151
+ format: uri
152
+ example: https://lh3.googleusercontent.com/...
153
+ description: User profile picture URL
154
+ description: Google OAuth user data
155
+ token:
156
+ type: string
157
+ example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
158
+ description: JWT token
159
+ apiKey:
160
+ type: string
161
+ example: ak_1234567890abcdef
162
+ description: User API key
163
+ apiToken:
164
+ type: string
165
+ example: at_1234567890abcdef
166
+ description: User API token
167
+ Organization:
168
+ type: object
169
+ properties:
170
+ id:
171
+ type: number
172
+ example: 1
173
+ description: Organization ID
174
+ name:
175
+ type: string
176
+ example: Acme Corp
177
+ description: Organization name
178
+ role:
179
+ type: string
180
+ example: OWNER
181
+ description: User role in the organization
182
+ OrganizationMember:
183
+ type: object
184
+ properties:
185
+ id:
186
+ type: number
187
+ example: 123
188
+ description: User ID
189
+ name:
190
+ type: string
191
+ example: John Doe
192
+ description: User name
193
+ email:
194
+ type: string
195
+ format: email
196
+ example: john@example.com
197
+ description: User email
198
+ role:
199
+ type: string
200
+ enum:
201
+ - MANAGER
202
+ - ADMIN
203
+ - OWNER
204
+ example: MANAGER
205
+ description: User role in the organization
206
+ AddMemberRequest:
207
+ type: object
208
+ required:
209
+ - email
210
+ properties:
211
+ email:
212
+ type: string
213
+ format: email
214
+ example: newmember@example.com
215
+ description: Email of the user to add
216
+ role:
217
+ type: string
218
+ enum:
219
+ - MANAGER
220
+ - ADMIN
221
+ - OWNER
222
+ example: MANAGER
223
+ default: MANAGER
224
+ description: Role to assign to the new member
225
+ UpdateRoleRequest:
226
+ type: object
227
+ required:
228
+ - role
229
+ properties:
230
+ role:
231
+ type: string
232
+ enum:
233
+ - MANAGER
234
+ - ADMIN
235
+ - OWNER
236
+ example: ADMIN
237
+ description: New role for the member
238
+ AnalyticsResponse:
239
+ type: object
240
+ properties:
241
+ orgId:
242
+ type: string
243
+ example: "1"
244
+ description: Organization ID
245
+ analytics:
246
+ type: object
247
+ properties:
248
+ totalClicks:
249
+ type: number
250
+ example: 1250
251
+ description: Total click count
252
+ dateRange:
253
+ type: object
254
+ properties:
255
+ from:
256
+ type: string
257
+ example: -7d
258
+ description: Start date of the range
259
+ to:
260
+ type: string
261
+ example: today
262
+ description: End date of the range
263
+ interval:
264
+ type: string
265
+ example: day
266
+ description: Data interval
267
+ data:
268
+ type: array
269
+ items:
270
+ type: number
271
+ example:
272
+ - 120
273
+ - 150
274
+ - 89
275
+ - 200
276
+ - 180
277
+ - 145
278
+ - 220
279
+ description: Click data points
280
+ labels:
281
+ type: array
282
+ items:
283
+ type: string
284
+ example:
285
+ - 2025-08-09
286
+ - 2025-08-10
287
+ - 2025-08-11
288
+ - 2025-08-12
289
+ - 2025-08-13
290
+ - 2025-08-14
291
+ - 2025-08-15
292
+ description: Date labels for data points
293
+ generatedAt:
294
+ type: string
295
+ format: date-time
296
+ example: 2025-08-15T12:00:00Z
297
+ description: Timestamp when analytics were generated
298
+ UpdateLinkRequest:
299
+ type: object
300
+ required:
301
+ - url
302
+ - key
303
+ properties:
304
+ url:
305
+ type: string
306
+ format: uri
307
+ example: https://updated-example.com
308
+ description: Updated URL
309
+ key:
310
+ type: string
311
+ example: updated-key
312
+ description: Updated short key
313
+ CreateOrganization:
314
+ type: object
315
+ required:
316
+ - name
317
+ properties:
318
+ name:
319
+ type: string
320
+ example: Acme Corp
321
+ description: Organization name
322
+ security:
323
+ - ApiKeyAuth: []
324
+ ApiSecretAuth: []
325
+ paths:
326
+ /v1/organizations:
327
+ get:
328
+ summary: Get user organizations
329
+ description: Retrieve organizations that the authenticated user has access to
330
+ tags:
331
+ - API v1 - Organizations
332
+ security:
333
+ - ApiKeyAuth: []
334
+ ApiSecretAuth: []
335
+ responses:
336
+ "200":
337
+ description: Successfully retrieved user organizations
338
+ content:
339
+ application/json:
340
+ schema:
341
+ type: array
342
+ items:
343
+ $ref: "#/components/schemas/Organization"
344
+ "401":
345
+ description: Unauthorized - Invalid API credentials
346
+ content:
347
+ application/json:
348
+ schema:
349
+ $ref: "#/components/schemas/Error"
350
+ post:
351
+ summary: Create a new organization
352
+ description: Create a new organization and assign the authenticated user as owner
353
+ tags:
354
+ - API v1 - Organizations
355
+ security:
356
+ - ApiKeyAuth: []
357
+ ApiSecretAuth: []
358
+ requestBody:
359
+ required: true
360
+ content:
361
+ application/json:
362
+ schema:
363
+ $ref: "#/components/schemas/CreateOrganization"
364
+ responses:
365
+ "200":
366
+ description: Successfully created organization
367
+ content:
368
+ application/json:
369
+ schema:
370
+ type: object
371
+ properties:
372
+ message:
373
+ type: string
374
+ example: Organization created successfully
375
+ id:
376
+ type: number
377
+ example: 1
378
+ description: Organization ID
379
+ name:
380
+ type: string
381
+ example: Acme Corp
382
+ description: Organization name
383
+ role:
384
+ type: string
385
+ example: OWNER
386
+ description: User role in the organization
387
+ "400":
388
+ description: Bad request - Name is required
389
+ content:
390
+ application/json:
391
+ schema:
392
+ $ref: "#/components/schemas/Error"
393
+ "401":
394
+ description: Unauthorized - Invalid API credentials
395
+ content:
396
+ application/json:
397
+ schema:
398
+ $ref: "#/components/schemas/Error"
399
+ "500":
400
+ description: Internal server error
401
+ content:
402
+ application/json:
403
+ schema:
404
+ $ref: "#/components/schemas/Error"
405
+ /v1/organizations/{orgId}/members:
406
+ get:
407
+ summary: Get organization members
408
+ description: Retrieve all members of a specific organization
409
+ tags:
410
+ - API v1 - Organizations
411
+ security:
412
+ - ApiKeyAuth: []
413
+ ApiSecretAuth: []
414
+ parameters:
415
+ - name: orgId
416
+ in: path
417
+ required: true
418
+ schema:
419
+ type: string
420
+ description: Organization ID
421
+ example: "1"
422
+ responses:
423
+ "200":
424
+ description: Successfully retrieved organization members
425
+ content:
426
+ application/json:
427
+ schema:
428
+ type: object
429
+ properties:
430
+ members:
431
+ type: array
432
+ items:
433
+ $ref: "#/components/schemas/OrganizationMember"
434
+ total:
435
+ type: number
436
+ example: 5
437
+ description: Total number of members
438
+ "401":
439
+ description: Unauthorized - Invalid API credentials
440
+ content:
441
+ application/json:
442
+ schema:
443
+ $ref: "#/components/schemas/Error"
444
+ "403":
445
+ description: Access denied to this organization
446
+ content:
447
+ application/json:
448
+ schema:
449
+ $ref: "#/components/schemas/Error"
450
+ post:
451
+ summary: Add member to organization
452
+ description: Add a new member to an organization (requires ADMIN or OWNER role)
453
+ tags:
454
+ - API v1 - Organizations
455
+ security:
456
+ - ApiKeyAuth: []
457
+ ApiSecretAuth: []
458
+ parameters:
459
+ - name: orgId
460
+ in: path
461
+ required: true
462
+ schema:
463
+ type: string
464
+ description: Organization ID
465
+ example: "1"
466
+ requestBody:
467
+ required: true
468
+ content:
469
+ application/json:
470
+ schema:
471
+ $ref: "#/components/schemas/AddMemberRequest"
472
+ responses:
473
+ "200":
474
+ description: Successfully added member to organization
475
+ content:
476
+ application/json:
477
+ schema:
478
+ type: object
479
+ properties:
480
+ message:
481
+ type: string
482
+ example: Member added successfully
483
+ member:
484
+ $ref: "#/components/schemas/OrganizationMember"
485
+ "400":
486
+ description: Bad request - Email is required or invalid role
487
+ content:
488
+ application/json:
489
+ schema:
490
+ $ref: "#/components/schemas/Error"
491
+ "401":
492
+ description: Unauthorized - Invalid API credentials
493
+ content:
494
+ application/json:
495
+ schema:
496
+ $ref: "#/components/schemas/Error"
497
+ "403":
498
+ description: Access denied - Requires ADMIN or OWNER role
499
+ content:
500
+ application/json:
501
+ schema:
502
+ $ref: "#/components/schemas/Error"
503
+ "404":
504
+ description: User not found - User must register first
505
+ content:
506
+ application/json:
507
+ schema:
508
+ $ref: "#/components/schemas/Error"
509
+ example:
510
+ error: User not found. User must register first.
511
+ "409":
512
+ description: Conflict - User is already a member
513
+ content:
514
+ application/json:
515
+ schema:
516
+ $ref: "#/components/schemas/Error"
517
+ "500":
518
+ description: Internal server error
519
+ content:
520
+ application/json:
521
+ schema:
522
+ $ref: "#/components/schemas/Error"
523
+ /v1/organizations/{orgId}/members/{memberId}/role:
524
+ patch:
525
+ summary: Update member role
526
+ description: Update the role of a specific member in an organization (requires
527
+ ADMIN or OWNER role)
528
+ tags:
529
+ - API v1 - Organizations
530
+ security:
531
+ - ApiKeyAuth: []
532
+ ApiSecretAuth: []
533
+ parameters:
534
+ - name: orgId
535
+ in: path
536
+ required: true
537
+ schema:
538
+ type: string
539
+ description: Organization ID
540
+ example: "1"
541
+ - name: memberId
542
+ in: path
543
+ required: true
544
+ schema:
545
+ type: string
546
+ description: Member ID
547
+ example: "123"
548
+ requestBody:
549
+ required: true
550
+ content:
551
+ application/json:
552
+ schema:
553
+ $ref: "#/components/schemas/UpdateRoleRequest"
554
+ responses:
555
+ "200":
556
+ description: Successfully updated member role
557
+ content:
558
+ application/json:
559
+ schema:
560
+ type: object
561
+ properties:
562
+ message:
563
+ type: string
564
+ example: Member role updated successfully
565
+ member:
566
+ type: object
567
+ properties:
568
+ id:
569
+ type: number
570
+ example: 123
571
+ name:
572
+ type: string
573
+ example: John Doe
574
+ email:
575
+ type: string
576
+ example: john@example.com
577
+ role:
578
+ type: string
579
+ example: ADMIN
580
+ previousRole:
581
+ type: string
582
+ example: MANAGER
583
+ "400":
584
+ description: Bad request - Role is required or invalid
585
+ content:
586
+ application/json:
587
+ schema:
588
+ $ref: "#/components/schemas/Error"
589
+ "401":
590
+ description: Unauthorized - Invalid API credentials
591
+ content:
592
+ application/json:
593
+ schema:
594
+ $ref: "#/components/schemas/Error"
595
+ "403":
596
+ description: Access denied - Requires ADMIN or OWNER role, or cannot change own
597
+ role
598
+ content:
599
+ application/json:
600
+ schema:
601
+ $ref: "#/components/schemas/Error"
602
+ "404":
603
+ description: Member not found in this organization
604
+ content:
605
+ application/json:
606
+ schema:
607
+ $ref: "#/components/schemas/Error"
608
+ example:
609
+ error: Member not found in this organization
610
+ "500":
611
+ description: Internal server error
612
+ content:
613
+ application/json:
614
+ schema:
615
+ $ref: "#/components/schemas/Error"
616
+ /v1/organizations/{orgId}/members/{memberId}:
617
+ delete:
618
+ summary: Remove member from organization
619
+ description: Remove a member from an organization (requires ADMIN or OWNER
620
+ role). Cannot remove yourself or the current OWNER.
621
+ tags:
622
+ - API v1 - Organizations
623
+ security:
624
+ - ApiKeyAuth: []
625
+ ApiSecretAuth: []
626
+ parameters:
627
+ - name: orgId
628
+ in: path
629
+ required: true
630
+ schema:
631
+ type: string
632
+ description: Organization ID
633
+ example: "1"
634
+ - name: memberId
635
+ in: path
636
+ required: true
637
+ schema:
638
+ type: string
639
+ description: Member ID
640
+ example: "123"
641
+ responses:
642
+ "200":
643
+ description: Successfully removed member from organization
644
+ content:
645
+ application/json:
646
+ schema:
647
+ type: object
648
+ properties:
649
+ message:
650
+ type: string
651
+ example: Member removed successfully
652
+ removedMember:
653
+ $ref: "#/components/schemas/OrganizationMember"
654
+ "401":
655
+ description: Unauthorized - Invalid API credentials
656
+ content:
657
+ application/json:
658
+ schema:
659
+ $ref: "#/components/schemas/Error"
660
+ "403":
661
+ description: Access denied - Requires ADMIN or OWNER role, cannot remove
662
+ yourself, or cannot remove last OWNER
663
+ content:
664
+ application/json:
665
+ schema:
666
+ $ref: "#/components/schemas/Error"
667
+ "404":
668
+ description: Member not found in this organization
669
+ content:
670
+ application/json:
671
+ schema:
672
+ $ref: "#/components/schemas/Error"
673
+ example:
674
+ error: Member not found in this organization
675
+ "500":
676
+ description: Internal server error
677
+ content:
678
+ application/json:
679
+ schema:
680
+ $ref: "#/components/schemas/Error"
681
+ /v1/organizations/{orgId}/analytics/clicks:
682
+ get:
683
+ summary: Get organization click analytics
684
+ description: Retrieve click analytics data for an organization's links from PostHog
685
+ tags:
686
+ - API v1 - Analytics
687
+ security:
688
+ - ApiKeyAuth: []
689
+ ApiSecretAuth: []
690
+ parameters:
691
+ - name: orgId
692
+ in: path
693
+ required: true
694
+ schema:
695
+ type: string
696
+ description: Organization ID
697
+ example: "1"
698
+ - name: date_from
699
+ in: query
700
+ required: false
701
+ schema:
702
+ type: string
703
+ description: "Start date for analytics (default: -7d)"
704
+ example: -7d
705
+ - name: date_to
706
+ in: query
707
+ required: false
708
+ schema:
709
+ type: string
710
+ description: "End date for analytics (default: today)"
711
+ example: today
712
+ - name: interval
713
+ in: query
714
+ required: false
715
+ schema:
716
+ type: string
717
+ description: "Data interval (default: day)"
718
+ example: day
719
+ responses:
720
+ "200":
721
+ description: Successfully retrieved analytics data
722
+ content:
723
+ application/json:
724
+ schema:
725
+ $ref: "#/components/schemas/AnalyticsResponse"
726
+ "401":
727
+ description: Unauthorized - Invalid API credentials
728
+ content:
729
+ application/json:
730
+ schema:
731
+ $ref: "#/components/schemas/Error"
732
+ "403":
733
+ description: Access denied to this organization
734
+ content:
735
+ application/json:
736
+ schema:
737
+ $ref: "#/components/schemas/Error"
738
+ "500":
739
+ description: Internal server error - Failed to fetch analytics data
740
+ content:
741
+ application/json:
742
+ schema:
743
+ $ref: "#/components/schemas/Error"
744
+ /v1/organizations/{orgId}/links:
745
+ get:
746
+ summary: Get organization links
747
+ description: Retrieve all links for a specific organization
748
+ tags:
749
+ - API v1 - Links
750
+ security:
751
+ - ApiKeyAuth: []
752
+ ApiSecretAuth: []
753
+ parameters:
754
+ - name: orgId
755
+ in: path
756
+ required: true
757
+ schema:
758
+ type: string
759
+ description: Organization ID
760
+ example: "1"
761
+ responses:
762
+ "200":
763
+ description: Successfully retrieved organization links
764
+ content:
765
+ application/json:
766
+ schema:
767
+ type: array
768
+ items:
769
+ $ref: "#/components/schemas/Link"
770
+ "401":
771
+ description: Unauthorized - Invalid API credentials
772
+ content:
773
+ application/json:
774
+ schema:
775
+ $ref: "#/components/schemas/Error"
776
+ "403":
777
+ description: Access denied to this organization
778
+ content:
779
+ application/json:
780
+ schema:
781
+ $ref: "#/components/schemas/Error"
782
+ post:
783
+ summary: Create a new link in organization
784
+ description: Create a new short link for the given URL in the specified organization
785
+ tags:
786
+ - API v1 - Links
787
+ security:
788
+ - ApiKeyAuth: []
789
+ ApiSecretAuth: []
790
+ parameters:
791
+ - name: orgId
792
+ in: path
793
+ required: true
794
+ schema:
795
+ type: string
796
+ description: Organization ID
797
+ example: "1"
798
+ requestBody:
799
+ required: true
800
+ content:
801
+ application/json:
802
+ schema:
803
+ $ref: "#/components/schemas/CreateLink"
804
+ responses:
805
+ "200":
806
+ description: Successfully created short link
807
+ content:
808
+ application/json:
809
+ schema:
810
+ $ref: "#/components/schemas/CreateLinkResponse"
811
+ "401":
812
+ description: Unauthorized - Invalid API credentials
813
+ content:
814
+ application/json:
815
+ schema:
816
+ $ref: "#/components/schemas/Error"
817
+ "403":
818
+ description: Access denied to this organization
819
+ content:
820
+ application/json:
821
+ schema:
822
+ $ref: "#/components/schemas/Error"
823
+ "500":
824
+ description: Internal server error
825
+ content:
826
+ application/json:
827
+ schema:
828
+ $ref: "#/components/schemas/Error"
829
+ /v1/organizations/{orgId}/links/{id}:
830
+ get:
831
+ summary: Get a specific link from organization
832
+ description: Retrieve a specific link by ID from the specified organization
833
+ tags:
834
+ - API v1 - Links
835
+ security:
836
+ - ApiKeyAuth: []
837
+ ApiSecretAuth: []
838
+ parameters:
839
+ - name: orgId
840
+ in: path
841
+ required: true
842
+ schema:
843
+ type: string
844
+ description: Organization ID
845
+ example: "1"
846
+ - name: id
847
+ in: path
848
+ required: true
849
+ schema:
850
+ type: string
851
+ description: Link ID
852
+ example: "1"
853
+ responses:
854
+ "200":
855
+ description: Successfully retrieved link
856
+ content:
857
+ application/json:
858
+ schema:
859
+ $ref: "#/components/schemas/Link"
860
+ "401":
861
+ description: Unauthorized - Invalid API credentials
862
+ content:
863
+ application/json:
864
+ schema:
865
+ $ref: "#/components/schemas/Error"
866
+ "403":
867
+ description: Access denied to this organization
868
+ content:
869
+ application/json:
870
+ schema:
871
+ $ref: "#/components/schemas/Error"
872
+ "404":
873
+ description: Link not found
874
+ content:
875
+ application/json:
876
+ schema:
877
+ $ref: "#/components/schemas/Error"
878
+ example:
879
+ error: Link not found
880
+ /v1/organizations/{orgId}/links/{linkId}:
881
+ put:
882
+ summary: Update a link in organization
883
+ description: Update an existing short link for the specified organization
884
+ tags:
885
+ - API v1 - Links
886
+ security:
887
+ - ApiKeyAuth: []
888
+ ApiSecretAuth: []
889
+ parameters:
890
+ - name: orgId
891
+ in: path
892
+ required: true
893
+ schema:
894
+ type: string
895
+ description: Organization ID
896
+ example: "1"
897
+ - name: linkId
898
+ in: path
899
+ required: true
900
+ schema:
901
+ type: string
902
+ description: Link ID
903
+ example: "1"
904
+ requestBody:
905
+ required: true
906
+ content:
907
+ application/json:
908
+ schema:
909
+ $ref: "#/components/schemas/UpdateLinkRequest"
910
+ responses:
911
+ "200":
912
+ description: Successfully updated link
913
+ content:
914
+ application/json:
915
+ schema:
916
+ type: object
917
+ properties:
918
+ message:
919
+ type: string
920
+ example: Link updated successfully
921
+ id:
922
+ type: string
923
+ example: "1"
924
+ description: Link ID
925
+ key:
926
+ type: string
927
+ example: updated-key
928
+ description: Updated short key
929
+ url:
930
+ type: string
931
+ format: uri
932
+ example: https://updated-example.com
933
+ description: Updated URL
934
+ shortLink:
935
+ type: string
936
+ example: https://zu.lk/updated-key
937
+ description: Full short link URL
938
+ updated_at:
939
+ type: string
940
+ format: date-time
941
+ example: 2025-08-15T12:00:00Z
942
+ description: Update timestamp
943
+ previousKey:
944
+ type: string
945
+ example: old-key
946
+ description: Previous short key
947
+ "400":
948
+ description: Bad request - URL and key are required
949
+ content:
950
+ application/json:
951
+ schema:
952
+ $ref: "#/components/schemas/Error"
953
+ "401":
954
+ description: Unauthorized - Invalid API credentials
955
+ content:
956
+ application/json:
957
+ schema:
958
+ $ref: "#/components/schemas/Error"
959
+ "403":
960
+ description: Access denied to this organization
961
+ content:
962
+ application/json:
963
+ schema:
964
+ $ref: "#/components/schemas/Error"
965
+ "404":
966
+ description: Link not found in this organization
967
+ content:
968
+ application/json:
969
+ schema:
970
+ $ref: "#/components/schemas/Error"
971
+ example:
972
+ error: Link not found in this organization
973
+ "409":
974
+ description: Conflict - Key is already taken
975
+ content:
976
+ application/json:
977
+ schema:
978
+ $ref: "#/components/schemas/Error"
979
+ "500":
980
+ description: Internal server error
981
+ content:
982
+ application/json:
983
+ schema:
984
+ $ref: "#/components/schemas/Error"