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:
POST /api/v1/tokens/generate
Authorization: Bearer EXISTING_TOKEN
Response:
{
"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
DELETE /api/v1/tokens/revoke
Authorization: Bearer YOUR_API_TOKEN
Response:
{
"success": true,
"message": "API token revoked successfully"
}
Links API
List Links
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:
{
"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
GET /api/v1/links/:id
Authorization: Bearer YOUR_API_TOKEN
Response: Same as a single link object above, plus an analytics field:
{
"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
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:
{
"success": true,
"link": { "id": 456, "short_code": "custom-code", "..." : "..." }
}
Update Link
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:
{
"success": true,
"link": { "..." : "..." }
}
Delete (Archive) Link
DELETE /api/v1/links/:id
Authorization: Bearer YOUR_API_TOKEN
Links are archived, not permanently deleted.
Response:
{
"success": true,
"message": "Link archived successfully"
}
Export Links (CSV)
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 collectioncreated_after(ISO 8601) — Links created after this datecreated_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)
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:
{
"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
GET /api/v1/links/:link_id/variants
Authorization: Bearer YOUR_API_TOKEN
Response:
{
"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
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:
{
"success": true,
"variant": { "id": 2, "name": "Variant B", "..." : "..." }
}
Update Variant
PATCH /api/v1/links/:link_id/variants/:variant_id
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json
{
"variant": {
"weight": 70,
"active": true
}
}
Response:
{
"success": true,
"variant": { "..." : "..." }
}
Delete Variant
DELETE /api/v1/links/:link_id/variants/:variant_id
Authorization: Bearer YOUR_API_TOKEN
Response:
{
"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
GET /api/v1/analytics/overview
Authorization: Bearer YOUR_API_TOKEN
Returns aggregated analytics across all your links for your configured attribution window.
Response:
{
"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
GET /api/v1/analytics/advanced
Authorization: Bearer YOUR_API_TOKEN
Response:
{
"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.
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 codeorder_id(string) — External order/transaction ID (used for deduplication)click_id(integer) — ClickAnalytic ID for explicit attributionip(string) — Client IP for automatic attribution (falls back to request IP)ua(string) — User agent for automatic attribution
Response:
{
"success": true,
"conversion_id": 456,
"attributed": true,
"value": 99.99,
"time_to_convert": "2.5 hours"
}
Error Responses:
404— Invalid postback token410— Conversion goal is inactive403— Feature not available for subscription tier409— Duplicate conversion (sameorder_idalready 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
{
"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:
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:
{
"success": false,
"error": "Human-readable error message"
}
Validation errors include an additional errors array:
{
"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.