API Documentation
Complete reference for the PVTLNK REST API
Related Document: Encryption Architecture Guide
Learn about our security architecture
PVTLNK API Documentation
Overview
PVTLNK provides a RESTful API for Enterprise subscribers to programmatically manage links, access analytics, and integrate with external systems.
Base URL: https://pvtlnk.com/api
Authentication: API Token (Enterprise only)
Authentication
All /api/v1/ endpoints require an API token. Include it in the Authorization header:
Authorization: Bearer YOUR_API_TOKEN
Generate API Token
Generate a token from API Settings in your dashboard, or programmatically:
http
POST /api/v1/tokens/generate
Authorization: Bearer EXISTING_TOKEN
Response:
json
{
"success": true,
"api_token": "abc123...",
"expires_at": "2027-03-12T00:00:00Z",
"message": "API token generated successfully. Store this token securely - it will not be shown again."
}
Revoke API Token
http
DELETE /api/v1/tokens/revoke
Authorization: Bearer YOUR_API_TOKEN
Response:
json
{
"success": true,
"message": "API token revoked successfully"
}
Links API
List Links
http
GET /api/v1/links
Authorization: Bearer YOUR_API_TOKEN
Query Parameters:
- page (integer) — Page number (default: 1)
- per_page (integer) — Results per page (default: 50, max: 100)
- active (boolean) — Filter to active links only (true)
- archived (boolean) — Filter to archived links only (true)
- scheduled (boolean) — Filter to scheduled links only (true)
- collection_id (integer) — Filter by link collection
Response:
json
{
"success": true,
"links": [
{
"id": 123,
"short_code": "abc123",
"short_url": "https://pvtlnk.com/abc123",
"destination_url": "https://example.com",
"title": "My Link",
"description": null,
"tags": [],
"click_count": 150,
"active": true,
"archived": false,
"password_protected": false,
"is_scheduled": false,
"scheduled_start_at": null,
"scheduled_end_at": null,
"expires": false,
"expires_at": null,
"last_clicked_at": "2026-03-10T14:22:00Z",
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-03-10T14:22:00Z",
"utm_params": {}
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 123,
"total_pages": 3
}
}
Get Single Link
http
GET /api/v1/links/:id
Authorization: Bearer YOUR_API_TOKEN
Response: Same as a single link object above, plus an analytics field:
json
{
"success": true,
"link": {
"id": 123,
"...": "...",
"analytics": {
"total_clicks": 150,
"recent_clicks": [
{
"clicked_at": "2026-03-10T14:22:00Z",
"hashed_ip": "a3f2...",
"hashed_user_agent": "b91c...",
"referrer_domain": "google.com",
"country_code": "CH",
"device_type": "desktop",
"browser_family": "Chrome",
"os_family": "macOS"
}
]
}
}
}
Create Link
```http POST /api/v1/links Authorization: Bearer YOUR_API_TOKEN Content-Type: application/json
{ “link”: { “destination_url”: “https://example.com”, “title”: “My Campaign Link”, “short_code”: “custom-code”, “scheduled_start_at”: “2026-04-01T00:00:00Z”, “scheduled_end_at”: “2026-04-30T23:59:59Z”, “expires”: true, “expires_at”: “2026-05-01T00:00:00Z”, “utm_params”: { “utm_source”: “newsletter”, “utm_medium”: “email”, “utm_campaign”: “apr-2026” } }, “password”: “optional-password” } ```
Note:
passwordis a top-level parameter, not nested insidelink. Scheduling (scheduled_start_at/scheduled_end_at) requires Pro or Enterprise plan.
Response:
json
{
"success": true,
"link": { "id": 456, "short_code": "custom-code", "..." : "..." }
}
Update Link
```http PATCH /api/v1/links/:id Authorization: Bearer YOUR_API_TOKEN Content-Type: application/json
{ “link”: { “destination_url”: “https://new-destination.com”, “title”: “Updated Title” } } ```
Response:
json
{
"success": true,
"link": { "..." : "..." }
}
Delete (Archive) Link
http
DELETE /api/v1/links/:id
Authorization: Bearer YOUR_API_TOKEN
Links are archived, not permanently deleted.
Response:
json
{
"success": true,
"message": "Link archived successfully"
}
Export Links (CSV)
http
GET /api/v1/links/export
Authorization: Bearer YOUR_API_TOKEN
Query Parameters:
- active (boolean) — Filter to active links only (true)
- archived (boolean) — Filter to archived links only (true)
- collection_id (integer) — Filter by link collection
- created_after (ISO 8601) — Links created after this date
- created_before (ISO 8601) — Links created before this date
Response: CSV file download (pvtlnk-links-YYYY-MM-DD.csv)
CSV columns: short_url, destination_url, short_code, title, description, tags, click_count, active, archived, password_protected, expires_at, created_at
Bulk Import Links (CSV)
```http POST /api/v1/links/bulk_import Authorization: Bearer YOUR_API_TOKEN Content-Type: application/json
{ “csv”: “destination_url,title,short_code,tags\nhttps://example1.com,Link 1,link1,\nhttps://example2.com,Link 2,link2,” } ```
Requires Enterprise plan. Send CSV as a string in the
csvfield.
CSV columns: destination_url (required), title, short_code, description, tags, active, password_protected, password, scheduled_start_at, scheduled_end_at, expires_at
Response:
json
{
"success": true,
"created": 2,
"links": [ { "..." : "..." } ],
"errors": [],
"summary": {
"total_rows": 2,
"created": 2,
"failed": 0
}
}
A/B Testing (Variants) API
Requires Pro or Enterprise plan.
List Variants
http
GET /api/v1/links/:link_id/variants
Authorization: Bearer YOUR_API_TOKEN
Response:
json
{
"success": true,
"ab_testing_active": true,
"variants": [
{
"id": 1,
"name": "Variant A",
"destination_url": "https://landing-a.com",
"weight": 50,
"active": true,
"click_count": 823,
"click_percentage": 51.2,
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
},
{
"id": 2,
"name": "Variant B",
"destination_url": "https://landing-b.com",
"weight": 50,
"active": true,
"click_count": 785,
"click_percentage": 48.8,
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}
]
}
Create Variant
```http POST /api/v1/links/:link_id/variants Authorization: Bearer YOUR_API_TOKEN Content-Type: application/json
{ “variant”: { “name”: “Variant B”, “destination_url”: “https://landing-b.com”, “weight”: 30 } } ```
Response:
json
{
"success": true,
"variant": { "id": 2, "name": "Variant B", "..." : "..." }
}
Update Variant
```http PATCH /api/v1/links/:link_id/variants/:variant_id Authorization: Bearer YOUR_API_TOKEN Content-Type: application/json
{ “variant”: { “weight”: 70, “active”: true } } ```
Response:
json
{
"success": true,
"variant": { "..." : "..." }
}
Delete Variant
http
DELETE /api/v1/links/:link_id/variants/:variant_id
Authorization: Bearer YOUR_API_TOKEN
Response:
json
{
"success": true,
"message": "Variant deleted successfully"
}
Traffic distribution: Weights are relative, not required to sum to 100. A link with variants at weight 70 and 30 will split traffic 70/30. Traffic always goes to at least one destination — if all variants are inactive, the link’s primary
destination_urlis used.
Analytics API
Overview
http
GET /api/v1/analytics/overview
Authorization: Bearer YOUR_API_TOKEN
Returns aggregated analytics across all your links for your configured attribution window.
Response:
json
{
"success": true,
"overview": {
"attribution_window_days": 30,
"period": {
"start_date": "2026-02-10T00:00:00Z",
"end_date": "2026-03-12T00:00:00Z"
},
"metrics": {
"total_clicks": 15234,
"unique_visitors": 12456,
"average_clicks_per_visitor": 1.22
},
"top_links": [
{
"id": 123,
"short_code": "abc123",
"title": "Popular Link",
"destination_url": "https://example.com",
"clicks": 2345,
"last_clicked_at": "2026-03-11T18:00:00Z"
}
],
"device_breakdown": { "desktop": 6234, "mobile": 8123, "tablet": 877 },
"country_breakdown": { "US": 5234, "CH": 3123, "DE": 2877 },
"referrer_breakdown": { "google.com": 3234, "twitter.com": 2123 },
"time_series": {
"2026-03-10": 523,
"2026-03-11": 612
}
}
}
The attribution window is set in API Settings. It cannot be overridden per request.
Advanced Analytics
http
GET /api/v1/analytics/advanced
Authorization: Bearer YOUR_API_TOKEN
Response:
json
{
"success": true,
"advanced": {
"attribution_window_days": 30,
"period": {
"start_date": "2026-02-10T00:00:00Z",
"end_date": "2026-03-12T00:00:00Z"
},
"conversion_analysis": [
{
"link_id": 123,
"short_code": "abc123",
"title": "My Link",
"clicks": 2345,
"created_at": "2026-01-15T10:30:00Z",
"conversion_rate": 12.5
}
],
"browser_breakdown": { "Chrome": 8234, "Safari": 4123, "Firefox": 2877 },
"os_breakdown": { "macOS": 5234, "Windows": 4123, "iOS": 3877 },
"hourly_distribution": { "0": 120, "1": 95, "..": "..", "23": 310 },
"day_of_week_distribution": { "0": 1200, "1": 2300, "..": "..", "6": 980 },
"insights": {
"peak_hour": 14,
"peak_day": 2,
"top_browser": "Chrome",
"top_os": "macOS"
},
"day_names": {
"0": "Sunday", "1": "Monday", "2": "Tuesday",
"3": "Wednesday", "4": "Thursday", "5": "Friday", "6": "Saturday"
}
}
}
Conversion Tracking API
Track Conversion (Postback)
Public endpoint — no authentication required. Uses a per-goal postback token.
```http POST /api/conversions/track/:token Content-Type: application/json
{ “value”: 99.99, “currency”: “USD”, “order_id”: “ORDER-12345”, “click_id”: 789, “ip”: “203.0.113.1”, “ua”: “Mozilla/5.0 …” } ```
URL Parameters:
- token — Postback token from your conversion goal
Body Parameters (all optional):
- value (decimal) — Conversion value (overrides goal default)
- currency (string) — 3-letter currency code
- order_id (string) — External order/transaction ID (used for deduplication)
- click_id (integer) — ClickAnalytic ID for explicit attribution
- ip (string) — Client IP for automatic attribution (falls back to request IP)
- ua (string) — User agent for automatic attribution
Response:
json
{
"success": true,
"conversion_id": 456,
"attributed": true,
"value": 99.99,
"time_to_convert": "2.5 hours"
}
Error Responses:
- 404 — Invalid postback token
- 410 — Conversion goal is inactive
- 403 — Feature not available for subscription tier
- 409 — Duplicate conversion (same order_id already tracked)
Outbound Webhooks
Enterprise users can configure a webhook URL to receive real-time event notifications.
Configuration
Set your Webhook URL and optional Webhook Secret in API Settings.
Event Types
| Event | Trigger |
|---|---|
link.created |
A short link is created |
link.updated |
A short link is updated |
link.clicked |
A short link is clicked |
conversion.tracked |
A conversion is recorded |
subscription.updated |
Subscription status changes |
Webhook Payload
json
{
"id": "a3f2b91c4d8e...",
"event": "link.clicked",
"timestamp": "2026-03-12T12:30:45Z",
"version": "1.0",
"data": {
"link": {
"id": 123,
"short_code": "abc123",
"title": "My Link"
},
"click": {
"id": 789,
"country_code": "CH",
"device_type": "mobile",
"browser_family": "Safari",
"os_family": "iOS",
"referrer_domain": "twitter.com",
"clicked_at": "2026-03-12T12:30:45Z"
},
"variant": null
}
}
Request Headers
| Header | Value |
|---|---|
X-PVTLNK-Event |
Event type (e.g. link.clicked) |
X-PVTLNK-Delivery |
Unique delivery ID |
X-PVTLNK-Signature |
sha256=<hmac_hex> (present if webhook secret is set) |
Signature Verification
If you have set a webhook secret, verify the X-PVTLNK-Signature header:
ruby
expected = OpenSSL::HMAC.hexdigest("SHA256", webhook_secret, request.raw_post)
provided = request.headers["X-PVTLNK-Signature"].sub("sha256=", "")
Rack::Utils.secure_compare(expected, provided)
Retry Policy
Failed deliveries are retried up to 3 times: - Attempt 1 — immediate - Attempt 2 — after 1 second - Attempt 3 — after 5 seconds - Final retry — after 30 seconds
Error Responses
All errors follow a consistent format:
json
{
"success": false,
"error": "Human-readable error message"
}
Validation errors include an additional errors array:
json
{
"success": false,
"error": "Validation failed",
"errors": ["Destination url can't be blank", "Short code has already been taken"]
}
HTTP Status Codes
| Code | Meaning |
|---|---|
200 |
Success |
201 |
Created |
400 |
Bad Request |
401 |
Unauthorized (missing or invalid token) |
403 |
Forbidden (feature not available on your plan) |
404 |
Not Found |
409 |
Conflict (duplicate) |
410 |
Gone (resource deactivated) |
422 |
Unprocessable Entity (validation failed) |
500 |
Internal Server Error |
Support
For API support, contact us through your Enterprise dashboard or at api@pvtlnk.com.