1. Introduction
All endpoints live under one base URL. Requests and responses are JSON; send Content-Type: application/json with a body.
https://2xurl.com/api/v1Bearer keys
Workspace API keys with the scopes you choose.
Fair limits
Per-minute and monthly limits from your plan, sent back as headers.
Signed webhooks
HMAC-SHA256 signatures on every delivery.
2. Authentication
- Create an API key in your workspace settings. It is shown once — store it somewhere safe.
- Keys start with
2x_and belong to one workspace, so you do not need to send a workspace id. - A key acts with the role of the member who created it. If that member is removed, the key stops working.
curl https://2xurl.com/api/v1/links \
-H "Authorization: Bearer 2x_your_key_here"Never put an API key in browser or mobile app code. Call the API from your server.
3. Scopes
Give each key only the scopes it needs. A request outside them is refused with 403.
- links.read
List and read links, folders and tags
- links.write
Create, update, archive and delete links
- qr.read
Read and download QR codes
- qr.write
Design QR codes and upload logos
- domains.read
List and read custom domains
- domains.write
Add, verify and remove domains
- campaigns.read
Read campaigns
- campaigns.write
Create and change campaigns
- bio.read
Read bio pages
- bio.write
Create and change bio pages
- analytics.read
Read click analytics
- workspaces.read
Read workspace details
- workspaces.write
Change workspace settings
- billing.read
Read wallet and usage
4. Rate Limits
- Each key has a per-minute limit and each workspace a monthly request quota, both set by your plan.
- Responses carry
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(seconds), plusX-Quota-LimitandX-Quota-Remaining. - Over a limit you get
429 rate_limitedwith aRetry-Afterheader. Wait that many seconds before retrying. - Send an
Idempotency-Keyheader on POST, PATCH and PUT requests so a retried request is not applied twice.
5. Errors
Errors use standard HTTP status codes and one JSON shape:
{
"error": { "code": "slug_taken", "message": "That back-half is already in use", "details": null },
"requestId": "a9116b45-2fbb-4e09-a359-5f3874e7dfe8"
}| 400 | validation_failed | A field is missing or invalid; see details |
| 400 | invalid_destination | The destination URL is malformed or not allowed |
| 400 | unsafe_destination | The destination is flagged by Google Safe Browsing |
| 400 | domain_not_verified | The domain is not verified yet |
| 401 | unauthorized | Missing, invalid or expired API key |
| 402 | insufficient_balance | Wallet is empty for a billable action |
| 403 | forbidden | Key lacks the scope or role for this action |
| 404 | not_found | The resource does not exist in this workspace |
| 409 | slug_taken | That back-half is already used on the domain |
| 429 | rate_limited | Too many requests; wait for Retry-After seconds |
| 500 | internal_error | Something went wrong on our side |
Include the requestId when you contact support about a failed request.
6. Pagination
Lists return up to 25 items by default and at most 100 (limit). Cursor-paginated lists answer with data, hasMore and nextCursor; pass cursor=nextCursor to get the next page.
{ "data": [ ... ], "hasMore": true, "nextCursor": "clx8k2..." }7. Links
| GET | /links | List links (cursor pagination; filter by search, folderId, campaignId, domainId) |
| POST | /links | Create a short link |
| GET | /links/{linkId} | Get one link |
| PATCH | /links/{linkId} | Update a link |
| PATCH | /links/{linkId}/archive | Archive or unarchive a link |
| POST | /links/{linkId}/duplicate | Duplicate a link |
| GET | /links/{linkId}/history | List changes made to a link |
| DELETE | /links/{linkId} | Delete a link |
| POST | /links/bulk-delete | Delete several links at once |
| POST | /links/import | Import links from CSV |
| GET | /links/export | Export links as CSV |
Create fields: destinationUrl (required), slug, domainId, title, description, folderId, campaignId, tagIds, password, redirectStatus, activeFrom, expiresAt, expiredUrl, maxClicks, and utmSource / utmMedium / utmCampaign / utmTerm / utmContent. Dates are ISO 8601.
curl -X POST https://2xurl.com/api/v1/links \
-H "Authorization: Bearer 2x_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"destinationUrl": "https://example.com/summer-sale",
"slug": "summer",
"utmSource": "newsletter",
"expiresAt": "2026-12-31T23:59:59Z"
}'The response is the link, including id, slug, shortUrl, clickCount, uniqueCount, hasPassword and tags. Counts are returned as strings.
8. QR Codes
| GET | /links/{linkId}/qr | Get a link’s QR design |
| POST | /links/{linkId}/qr/preview | Preview a design without saving |
| PUT | /links/{linkId}/qr | Save a QR design |
| GET | /links/{linkId}/qr/download | Download the QR code |
| POST | /qr/bulk | Generate QR codes for many links |
| POST | /qr/logo | Upload a logo for QR codes |
9. Domains
Coming soon. Custom domains are paused while we finish and test them; these endpoints are listed for reference and may refuse requests until then.
| GET | /domains | List domains |
| POST | /domains | Add a domain |
| GET | /domains/{domainId} | Get one domain and its DNS records |
| PATCH | /domains/{domainId} | Update a domain |
| POST | /domains/{domainId}/verify | Check DNS and verify the domain |
| POST | /domains/{domainId}/default | Make it the workspace default |
| DELETE | /domains/{domainId} | Remove a domain |
A domain must be verified before links can use it; otherwise you get domain_not_verified.
10. Analytics
| GET | /analytics/overview | Clicks and unique clicks over time |
| GET | /analytics/breakdown | Clicks grouped by a dimension |
| GET | /analytics/top-links | Most-clicked links |
| GET | /analytics/top-campaigns | Most-clicked campaigns |
| GET | /analytics/events | Recent click events (cursor pagination) |
| GET | /analytics/export | Export analytics as CSV |
Common query parameters: from, to (ISO dates), tz (IANA time zone such as Asia/Kolkata), linkId, domainId. breakdown takes a dimension: country, device, browser, os, referrer, utm_source, utm_medium, utm_campaign or source.
curl "https://2xurl.com/api/v1/analytics/breakdown?dimension=country&linkId=LINK_ID&from=2026-10-01&to=2026-10-16" \
-H "Authorization: Bearer 2x_your_key_here"11. Webhooks
| GET | /webhooks/events | List the events you can subscribe to |
| GET | /webhooks | List webhooks |
| POST | /webhooks | Create a webhook |
| PATCH | /webhooks/{webhookId} | Update a webhook |
| DELETE | /webhooks/{webhookId} | Delete a webhook |
| GET | /webhooks/{webhookId}/deliveries | List recent deliveries |
| POST | /webhooks/{webhookId}/deliveries/{deliveryId}/retry | Retry a delivery |
Events: link.created, link.updated, link.deleted, domain.verified, workspace.member_invited, workspace.member_joined.
- Each delivery is a JSON POST with headers
X-2xurl-Event,X-2xurl-DeliveryandX-2xurl-Signature. - The signature is
sha256=followed by the HMAC-SHA256 of the raw body, keyed with your webhook secret. - Answer with a 2xx status. Failed deliveries are retried up to 6 times with exponential backoff; a webhook that keeps failing is disabled automatically.
See the webhooks guide for a verification example.
12. Support
Questions about the API? Email [email protected] with the requestId of the request, or check the Status page if something looks down.
