Reference
API reference
A JSON API for everything in the dashboard: links, analytics, plans, API keys and workspaces.
On this page
Introduction
- Base URL:
https://furl.io/api/v1 - JSON only. Request bodies need
Content-Type: application/jsonand may be at most 64 KB. - Unknown JSON fields are rejected with
400 bad_request, so typos don’t pass silently. - Timestamps are RFC3339 in UTC, e.g.
2026-10-05T12:34:56Z.
OpenAPI specification
Authentication
Send any one of the following:
| Method | How | Notes |
|---|---|---|
| Session cookie | furl_session, set by login and register | HttpOnly, Secure, SameSite=Lax, 7 days. Cookie-authenticated POST/PATCH/DELETE requests from another origin are rejected with 403 csrf. |
| Login token | Authorization: Bearer <token> | The token returned by login or register. A JWT, valid 7 days. |
| API key | Authorization: Bearer furl_… or X-API-Key: furl_… | 45 characters, starting with furl_. Shown once at creation. |
# API key, either header works
curl -s https://furl.io/api/v1/me -H "Authorization: Bearer $FURL_API_KEY"
curl -s https://furl.io/api/v1/me -H "X-API-Key: $FURL_API_KEY"
# Token from /auth/login or /auth/register (valid 7 days)
curl -s https://furl.io/api/v1/me -H "Authorization: Bearer $FURL_TOKEN"
# Session cookie (what the dashboard uses)
curl -s -c cookies.txt https://furl.io/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"correct horse battery"}'
curl -s -b cookies.txt https://furl.io/api/v1/meWorkspaces and roles
Links, API keys, analytics, the plan and billing belong to a workspace. Every account has a personal workspace; team workspaces are shared with other members. Requests made with a session cookie or login token act on your personal workspace unless you choose another with the X-Furl-Workspace header:
# List the workspaces you belong to
curl -s https://furl.io/api/v1/workspaces -H "Authorization: Bearer $FURL_TOKEN"
# Act on a team workspace
curl -s https://furl.io/api/v1/links \
-H "Authorization: Bearer $FURL_TOKEN" \
-H "X-Furl-Workspace: 9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a"- The value is a workspace
idfrom /me or /workspaces. Naming a workspace you don’t belong to returns403 not_member(except on/me, which falls back to your personal workspace). - API keys ignore the header: a key always acts on the workspace it was created in.
| Role | Can |
|---|---|
| member | Create, edit and delete any link in the workspace; read its analytics. |
| admin | Everything a member can, plus API keys, invitations, members, custom domains and renaming the workspace. |
| owner | Everything an admin can, plus billing, transferring ownership and deleting the workspace. |
Each workspace has exactly one owner. A request your role doesn’t allow returns 403 insufficient_role.
Errors
Errors return a JSON body with a stable, machine-readable code and a human-readable message. Match on the code; the message may change.
{
"error": {
"code": "code_taken",
"message": "short code already taken"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed JSON, unknown fields, or invalid query parameters. |
| 401 | unauthorized | Missing, invalid or expired credentials. |
| 401 | invalid_credentials | Wrong email or password (login). |
| 403 | plan_limit_reached | Your plan’s active link limit is reached (creating or resuming a link). |
| 403 | custom_code_limit | Your plan’s custom code allowance is used up. |
| 403 | link_expiry_not_in_plan | Your plan doesn’t include expiry dates and view limits (Pro and higher). |
| 403 | integrations_not_in_plan | Your plan doesn’t include webhooks and GA4 forwarding (Pro and higher). |
| 403 | csrf | Cookie-authenticated write from another origin. |
| 403 | not_purchasable | The plan or extra custom codes can’t be bought on your account. |
| 403 | managed_account | Your plan is managed by furl.io, so there’s nothing to pay (checkout). |
| 403 | admin_account | Administrator accounts manage the service and can’t create links, API keys or purchases. Use a regular account. |
| 403 | code_blocked | The custom short code is reserved. |
| 403 | invalid_password | Wrong current password (account deletion). |
| 403 | email_unverified | The account’s email isn’t verified yet. |
| 403 | session_required | Needs a session or login token, not an API key (account deletion, billing). |
| 403 | not_member | X-Furl-Workspace names a workspace you don’t belong to. |
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 403 | member_limit | Every seat on the workspace’s plan is taken (members plus open invitations). |
| 403 | domain_limit | The workspace’s plan doesn’t include (more) custom domains. |
| 403 | invite_email_mismatch | The invitation was sent to a different email address. |
| 404 | invite_invalid | Unknown, expired, withdrawn or already used invitation. |
| 400 | signature_mismatch | The payment signature doesn’t match (billing verify). |
| 400 | payment_mismatch | The payment doesn’t match the order (billing verify). |
| 409 | payment_not_captured | The payment hasn’t been captured yet (billing verify). |
| 404 | not_found | Not found — also returned for links you don’t own. |
| 409 | code_taken | The custom short code is in use. |
| 409 | email_taken | An account with this email exists. |
| 409 | already_verified | The email is already verified. |
| 409 | personal_workspace | Not possible in a personal workspace (rename, delete, invitations). |
| 409 | owner_required | The workspace must keep exactly one owner; transfer ownership first. |
| 409 | already_member | The invited person is already a member. |
| 409 | domain_taken | The custom domain is already added to a workspace. |
| 409 | domain_in_use | Links still use the custom domain. |
| 409 | workspace_has_members | The account owns a team workspace with other members (account deletion). Transfer ownership or delete it first. |
| 410 | otp_expired | Verification code expired or too many wrong attempts. |
| 422 | terms_required | Registration without accept_terms: true. |
| 422 | otp_invalid | Wrong verification code (includes attempts_left). |
| 422 | validation_error | A value is well-formed but not allowed (see the message). |
| 429 | otp_rate_limited | Verification code requested too soon (includes retry_after). |
| 429 | — | Too Many Requests (plain-text body, not JSON). |
| 500 | internal | Something went wrong on our side. |
| 503 | email_send_failed | The verification email couldn’t be sent. |
| 503 | email_unavailable | Email sending is unavailable (verification codes, invitations). |
| 503 | billing_unavailable | Online payments are temporarily unavailable. |
Rate limits
Limits are per client IP address. Exceeding them returns 429 with a plain-text body.
| Traffic | Limit | Burst |
|---|---|---|
/api/v1/auth/* | 10 requests per minute | 10 |
| Other API endpoints | 20 requests per second | 40 |
| Redirects (furl.io/{code}) | 50 requests per second | 100 |
| Dashboard and docs pages | 30 requests per second | 60 |
Analytics endpoints (/links/{code}/stats* and /stats/overview) also allow at most 4 requests in flight per account; more return 429 too_many_concurrent_requests with Retry-After: 1. Verification emails are subject to a platform-wide budget; when it is exhausted, 503 email_unavailable is returned with Retry-After.
Pagination
List endpoints take limit (default 50, max 200) and cursor. Pass the previous response’s next_cursor as cursor to get the next page. When next_cursor is null, you’ve reached the last page.
cursor=""
while :; do
page=$(curl -s "https://furl.io/api/v1/links?limit=200&cursor=$cursor" \
-H "Authorization: Bearer $FURL_API_KEY")
echo "$page" | jq -r '.items[].short_url'
cursor=$(echo "$page" | jq -r '.next_cursor // empty')
[ -z "$cursor" ] && break
doneObjects
Link
| Field | Type | Description |
|---|---|---|
| id | integer | Numeric id. |
| short_code | string | The code after furl.io/. |
| short_url | string | The full short URL. |
| original_url | string | The destination. |
| is_custom | boolean | Whether the code was chosen by the owner. |
| status | string | active | paused | archived | banned |
| http_status_code | integer | 301 | 302 | 307 | 308 |
| google_analytics_id | string | null | GA4 measurement ID. |
| has_ga_secret | boolean | Whether a GA4 API secret is saved (the secret is never returned). |
| webhook_url | string | null | Webhook endpoint. |
| webhook_secret | string | null | Signing secret (whsec_…), generated automatically when a webhook URL is first set. |
| expires_at | RFC3339 | null | When the link stops redirecting; null = never expires (the default). |
| max_clicks | integer | null | View limit (1–100,000); null = unlimited. |
| clicks_used | integer | Views consumed so far. Only counted while max_clicks is set; link previews and HEAD requests don’t count. |
| created_at | RFC3339 | |
| updated_at | RFC3339 |
{
"id": 42,
"short_code": "launch",
"short_url": "https://furl.io/launch",
"original_url": "https://example.com/spring-sale?utm_source=newsletter",
"is_custom": true,
"status": "active",
"http_status_code": 302,
"google_analytics_id": "G-ABC123XYZ9",
"has_ga_secret": true,
"webhook_url": "https://hooks.example.com/furl",
"webhook_secret": "whsec_9f2c4e1a7b3d5f60a8c2e4b6d8f0a1c3",
"expires_at": null,
"max_clicks": null,
"clicks_used": 0,
"created_at": "2026-10-01T09:30:00Z",
"updated_at": "2026-10-05T12:00:00Z"
}User
tier is your plan’s code (free, pro, enterprise, custom, or the code of a private plan). email_verified is false until the code emailed at registration is confirmed. managed is true when your plan is assigned by furl.io, with nothing to pay. tier, managed, plan and usage describe the current workspace. /me also returns the current workspace and all your workspaces.
{
"id": "3f6c2a9e-5b1d-4c47-9a2e-7d0b8f1e4a63",
"email": "[email protected]",
"tier": "free",
"is_admin": false,
"managed": false,
"email_verified": true,
"email_verification_required": false,
"created_at": "2026-10-01T09:00:00Z"
}Plan
| Field | Type | Description |
|---|---|---|
| code | string | Plan identifier, same as the user’s tier. |
| name | string | Display name. |
| description | string | Short description. |
| price_monthly | number | Price per month in US dollars, excluding taxes. 0 when price_on_request is true. |
| price_on_request | boolean | The plan is arranged with our sales team and can’t be bought online; it has no listed price. |
| currency | string | Always USD. |
| max_active_links | integer | null | How many links can be active (and not expired) at once; null = unlimited. |
| link_expiry | boolean | Links can be given an expiry date or view limit. |
| integrations | boolean | Links can have a webhook and GA4 forwarding, and clicks are forwarded. |
| custom_codes_included | integer | null | Custom short codes included; 0 = none, null = unlimited. |
| custom_code_price | number | null | USD per extra custom code per month; null = not available. |
| max_members | integer | null | Seats per workspace (members plus open invitations); null = unlimited. |
| max_domains | integer | null | Custom domains per workspace; 0 = not included, null = unlimited. |
| features | string[] | Highlights shown on the pricing page. |
| tax_note | string | Shown next to the price, e.g. “+ applicable taxes”. |
| version | integer | Increases each time the plan’s terms change. |
| effective_from | RFC3339 | When this version took effect. |
{
"code": "pro",
"name": "Pro",
"description": "For teams sharing links every day.",
"price_monthly": 12,
"price_on_request": false,
"currency": "USD",
"max_active_links": 300,
"link_expiry": true,
"integrations": true,
"custom_codes_included": 30,
"custom_code_price": 1,
"max_members": 3,
"max_domains": 1,
"features": ["Everything in Free", "Webhooks and GA4 forwarding", "Expiry by date or number of clicks", "Email support"],
"tax_note": "+ applicable taxes",
"version": 3,
"effective_from": "2026-10-01T00:00:00Z"
}ApiKey
key_prefix identifies the key without revealing it. last_used_at is null until first use.
{
"id": "b7e1d0c2-4f3a-4e8b-9c6d-1a2b3c4d5e6f",
"name": "ci-deploy",
"key_prefix": "furl_k3Jd9",
"is_active": true,
"created_at": "2026-10-02T08:00:00Z",
"last_used_at": "2026-10-05T11:58:12Z"
}Redirect behaviour
A visitor opening https://furl.io/{code} gets:
| Link state | Response |
|---|---|
| Active | 301, 302, 307 or 308 (the link’s redirect type) to the destination |
| Paused, or no such code | 404 Not Found |
| Archived, or past expires_at | 410 Gone |
| View limit reached (clicks_used ≥ max_clicks) | 410 Gone (“this link has reached its view limit”) |
| Banned | 451 Unavailable For Legal Reasons |
For links with a view limit (max_clicks), each redirect increments clicks_used. Link-preview bots (Slack, WhatsApp, Telegram, Discord, Facebook, X/Twitter, LinkedIn…) and HEAD requests get a neutral page instead: it doesn’t consume a view or reveal the destination. View-limited links are never cached by browsers, even with a 301 or 308 redirect type.
Authentication and account
Version
/versionAuth: None
The running release. Public; useful for health checks and bug reports.
Response
{
"version": "1.4.0",
"commit": "9f2c4e1a7b3d5f60a8c2e4b6d8f0a1c3e5f7a9b1"
}Errors
Plus the common errors.
curl -s https://furl.io/api/v1/versionRegister
/auth/registerAuth: None
Creates an account on the Free plan and signs it in: the response sets the furl_session cookie and also returns a token. A 6-digit verification code is emailed; verification_email_sent says whether that worked. Confirm it with verify email.
JSON body
| Name | Type | Description |
|---|---|---|
| emailrequired | string | A valid email address. |
| passwordrequired | string | 8–72 characters. |
| accept_termsrequired | boolean | Must be true: the user accepts the Terms of Use and Privacy Policy. The accepted version and time are recorded. |
Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "3f6c2a9e-5b1d-4c47-9a2e-7d0b8f1e4a63",
"email": "[email protected]",
"tier": "free",
"is_admin": false,
"managed": false,
"email_verified": false,
"email_verification_required": false,
"created_at": "2026-10-01T09:00:00Z"
},
"verification_email_sent": true
}Errors
| Status | Code | When |
|---|---|---|
| 409 | email_taken | An account with this email already exists. |
| 422 | terms_required | accept_terms is missing or not true. |
| 422 | validation_error | Invalid email, or password outside 8–72 characters. |
Plus the common errors.
curl -s https://furl.io/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"correct horse battery","accept_terms":true}'Log in
/auth/loginAuth: None
Sets the furl_session cookie and returns a token (a JWT valid for 7 days) you can send as Authorization: Bearer <token>.
JSON body
| Name | Type | Description |
|---|---|---|
| emailrequired | string | Account email. |
| passwordrequired | string | Account password. |
Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "3f6c2a9e-5b1d-4c47-9a2e-7d0b8f1e4a63",
"email": "[email protected]",
"tier": "free",
"is_admin": false,
"managed": false,
"email_verified": true,
"email_verification_required": false,
"created_at": "2026-10-01T09:00:00Z"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | invalid_credentials | Wrong email or password. |
Plus the common errors.
curl -s https://furl.io/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"correct horse battery"}'Verify email
/auth/verify-emailAuth: Session cookie or login token
Confirms the 6-digit code emailed at registration. Codes expire after 10 minutes or 5 wrong attempts. When the server enforces verification, unverified users get 403 email_unverified from every endpoint except /me, /auth/* and DELETE /me.
JSON body
| Name | Type | Description |
|---|---|---|
| coderequired | string | The 6 digits from the email. Spaces and dashes are tolerated. |
Response
{
"id": "3f6c2a9e-5b1d-4c47-9a2e-7d0b8f1e4a63",
"email": "[email protected]",
"tier": "free",
"is_admin": false,
"managed": false,
"email_verified": true,
"email_verification_required": false,
"created_at": "2026-10-01T09:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 422 | otp_invalid | Wrong code. The error object includes attempts_left. |
| 422 | validation_error | Not 6 digits. |
| 410 | otp_expired | Expired, or 5 wrong attempts. Request a new code. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/auth/verify-email \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"code":"123456"}'Resend verification code
/auth/resend-verificationAuth: Session cookie or login token
Sends a new code (the old one stops working). No body. Limited to one per 60 seconds and five per hour.
Response
{
"sent": true,
"expires_in": 600
}Errors
| Status | Code | When |
|---|---|---|
| 429 | otp_rate_limited | Too soon. The error object includes retry_after (seconds); the Retry-After header is set too. |
| 409 | already_verified | The email is already verified. |
| 503 | email_send_failed | The email provider rejected or failed the send. Try again. |
| 503 | email_unavailable | Email sending isn’t configured or is temporarily unavailable. |
Plus the common errors (including 401 unauthorized).
curl -s -X POST https://furl.io/api/v1/auth/resend-verification \
-H "Authorization: Bearer $FURL_TOKEN"Log out
/auth/logoutAuth: None
Clears the furl_session cookie. Login tokens can’t be revoked; they expire after 7 days, so discard them on your side.
Response
204 No Content with an empty body.
Errors
Plus the common errors.
curl -s -X POST https://furl.io/api/v1/auth/logout -b cookies.txt -c cookies.txtCurrent user
/meAuth: Session cookie, login token or API key (details)
Returns the authenticated user, the current workspace (chosen with X-Furl-Workspace; if that names a workspace you’re no longer in, your personal workspace), every workspace you belong to in workspaces (personal first), and the current workspace’s plan (see Plan) and usage: active_links (links that are active and not expired), custom_codes (links created with a custom code) and custom_code_addon (extra custom codes added to the plan). Custom codes allowed = plan.custom_codes_included + usage.custom_code_addon, or unlimited when custom_codes_included is null.
Response
{
"id": "3f6c2a9e-5b1d-4c47-9a2e-7d0b8f1e4a63",
"email": "[email protected]",
"tier": "pro",
"is_admin": false,
"managed": false,
"email_verified": true,
"email_verification_required": false,
"created_at": "2026-10-01T09:00:00Z",
"workspace": {
"id": "9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a",
"name": "Acme Marketing",
"slug": "acme-marketing",
"personal": false,
"role": "owner",
"tier": "pro",
"managed": false,
"custom_code_addon": 0,
"plan_paid_until": "2027-01-04T10:00:00Z",
"created_at": "2026-10-02T08:00:00Z"
},
"workspaces": [
{
"id": "5c2e8f0a-7d1b-4a3c-9e6f-2b4d6f8a0c1e",
"name": "Personal",
"slug": "personal-3f6c2a9e",
"personal": true,
"role": "owner",
"tier": "free",
"managed": false,
"custom_code_addon": 0,
"plan_paid_until": null,
"created_at": "2026-10-01T09:00:00Z"
},
{
"id": "9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a",
"name": "Acme Marketing",
"slug": "acme-marketing",
"personal": false,
"role": "owner",
"tier": "pro",
"managed": false,
"custom_code_addon": 0,
"plan_paid_until": "2027-01-04T10:00:00Z",
"created_at": "2026-10-02T08:00:00Z"
}
],
"plan": {
"code": "pro",
"name": "Pro",
"description": "For teams sharing links every day.",
"price_monthly": 12,
"price_on_request": false,
"currency": "USD",
"max_active_links": 300,
"link_expiry": true,
"integrations": true,
"custom_codes_included": 30,
"custom_code_price": 1,
"max_members": 3,
"max_domains": 1,
"features": ["Everything in Free", "Webhooks and GA4 forwarding", "Expiry by date or number of clicks", "Email support"],
"tax_note": "+ applicable taxes",
"version": 3,
"effective_from": "2026-10-01T00:00:00Z"
},
"usage": {
"active_links": 42,
"custom_codes": 3,
"custom_code_addon": 0
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | Missing, invalid or expired credentials. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/me \
-H "Authorization: Bearer $FURL_API_KEY"Delete account
/meAuth: Session cookie or login token only. API keys are refused (403 session_required).
Permanently erases the account, its personal workspace and any workspace where it’s the only member, with their links (which stop redirecting immediately), API keys, webhook delivery logs and the links' click analytics. Links the account created in shared team workspaces stay with the team. Some records are kept as the law requires; see the Privacy Policy.
JSON body
| Name | Type | Description |
|---|---|---|
| passwordrequired | string | The account’s current password. |
Response
204 No Content with an empty body.
Errors
| Status | Code | When |
|---|---|---|
| 403 | invalid_password | The password is wrong. |
| 403 | session_required | The request used an API key. |
| 409 | workspace_has_members | The account owns a team workspace with other members. Transfer ownership or delete that workspace first. |
Plus the common errors (including 401 unauthorized).
curl -s -X DELETE https://furl.io/api/v1/me \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"password":"correct horse battery"}'Plans
List plans
/plansAuth: None
The plans on the pricing page, in display order. Public; no authentication needed. Your own plan, with your usage, is on /me.
Response
{
"items": [
{
"code": "pro",
"name": "Pro",
"description": "For teams sharing links every day.",
"price_monthly": 12,
"price_on_request": false,
"currency": "USD",
"max_active_links": 300,
"link_expiry": true,
"integrations": true,
"custom_codes_included": 30,
"custom_code_price": 1,
"max_members": 3,
"max_domains": 1,
"features": ["Everything in Free", "Webhooks and GA4 forwarding", "Expiry by date or number of clicks", "Email support"],
"tax_note": "+ applicable taxes",
"version": 3,
"effective_from": "2026-10-01T00:00:00Z"
}
]
}Errors
Plus the common errors.
curl -s https://furl.io/api/v1/plansBilling
Pay for a plan or for extra custom codes with Razorpay Standard Checkout. Amounts are in cents (US dollars). The price is the plan price (or the per-code price × quantity) × months, plus 18% GST when billing_country is IN. Periods are prepaid 30-day months with no automatic renewal: paying for the same plan or quantity again extends from the current end date, and switching plans starts now. When a period ends the workspace returns to the Free plan (or the extra codes to 0). Billing applies to the current workspace; buying needs the owner role (403 insufficient_role otherwise), and the history lists the workspace’s orders. These endpoints need a signed-in session; API keys are refused.
Create an order
/billing/ordersAuth: Session cookie or login token only. API keys are refused (403 session_required).
Creates a Razorpay order and returns what Razorpay Checkout needs (key_id, order_id, amount, currency, name, description, prefill) plus the price breakdown. Open the checkout with these values, then send its result to verify.
JSON body
| Name | Type | Description |
|---|---|---|
| kindrequired | string | plan or custom_codes. |
| plan_code | string | With kind: plan: the plan to buy (a paid public plan). |
| quantity | integer | With kind: custom_codes: number of extra custom codes, 1–100,000. |
| monthsrequired | integer | Period to pay for, 1–12 (30-day months). |
| billing_countryrequired | string | ISO 3166-1 alpha-2 country code, e.g. IN or US. |
| gstin | string | Optional GSTIN; only with billing_country: IN. |
Response
{
"order_id": "order_PQ7sYdK2aB3cDe",
"amount": 6726,
"currency": "USD",
"key_id": "rzp_live_XXXXXXXXXXXXXX",
"name": "furl.io",
"description": "Pro plan · 3 months",
"prefill": { "email": "[email protected]" },
"breakdown": {
"unit_price": 1900,
"quantity": 1,
"months": 3,
"subtotal": 5700,
"tax_rate": 18,
"tax": 1026,
"total": 6726,
"currency": "USD",
"tax_label": "GST 18%"
}
}Errors
| Status | Code | When |
|---|---|---|
| 403 | not_purchasable | The plan can’t be bought online (the Free plan, or a plan with price_on_request, which is arranged with our sales team), or extra custom codes aren’t offered on your plan. |
| 403 | managed_account | Your plan is managed by furl.io, so there’s nothing to pay. |
| 403 | insufficient_role | Only the workspace owner can pay. |
| 403 | session_required | The request used an API key. |
| 422 | validation_error | A missing or invalid field (months, quantity, country, GSTIN…). |
| 503 | billing_unavailable | Online payments are temporarily unavailable. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/billing/orders \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"kind":"plan","plan_code":"pro","months":3,"billing_country":"IN"}'Verify a payment
/billing/verifyAuth: Session cookie or login token only. API keys are refused (403 session_required).
Send the three values Razorpay Checkout passes to its success handler. When the signature and payment check out, the order is marked paid and the plan or extra codes take effect immediately.
JSON body
| Name | Type | Description |
|---|---|---|
| razorpay_order_idrequired | string | From the checkout result. |
| razorpay_payment_idrequired | string | From the checkout result. |
| razorpay_signaturerequired | string | From the checkout result. |
Response
{
"id": "ord_01J9Z6Q4X1",
"status": "paid",
"kind": "plan",
"plan_code": "pro",
"quantity": null,
"months": 3,
"razorpay_order_id": "order_PQ7sYdK2aB3cDe",
"razorpay_payment_id": "pay_PQ7tA1b2C3d4E5",
"billing_country": "IN",
"gstin": null,
"breakdown": {
"unit_price": 1900,
"quantity": 1,
"months": 3,
"subtotal": 5700,
"tax_rate": 18,
"tax": 1026,
"total": 6726,
"currency": "USD",
"tax_label": "GST 18%"
},
"period_start": "2026-10-06T10:00:00Z",
"period_end": "2027-01-04T10:00:00Z",
"created_at": "2026-10-06T09:58:41Z",
"paid_at": "2026-10-06T10:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 400 | signature_mismatch | The signature doesn’t match the order and payment. |
| 400 | payment_mismatch | The payment doesn’t belong to this order or the amount differs. |
| 404 | not_found | No such order on your account. |
| 409 | payment_not_captured | The payment hasn’t been captured yet. Try again shortly. |
| 503 | billing_unavailable | Online payments are temporarily unavailable. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/billing/verify \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"razorpay_order_id":"order_PQ7sYdK2aB3cDe","razorpay_payment_id":"pay_PQ7tA1b2C3d4E5","razorpay_signature":"9d1c…"}'Billing history
/billing/ordersAuth: Session cookie or login token only. API keys are refused (403 session_required).
Your orders, newest first (status is created, paid or failed), with the end of the current paid plan period (plan_paid_until) and extra-codes period (addon_paid_until), and whether online payments are available (payments_enabled).
Response
{
"items": [
{
"id": "ord_01J9Z6Q4X1",
"status": "paid",
"kind": "plan",
"plan_code": "pro",
"quantity": null,
"months": 3,
"razorpay_order_id": "order_PQ7sYdK2aB3cDe",
"razorpay_payment_id": "pay_PQ7tA1b2C3d4E5",
"billing_country": "IN",
"gstin": null,
"breakdown": {
"unit_price": 1900,
"quantity": 1,
"months": 3,
"subtotal": 5700,
"tax_rate": 18,
"tax": 1026,
"total": 6726,
"currency": "USD",
"tax_label": "GST 18%"
},
"period_start": "2026-10-06T10:00:00Z",
"period_end": "2027-01-04T10:00:00Z",
"created_at": "2026-10-06T09:58:41Z",
"paid_at": "2026-10-06T10:00:00Z"
}
],
"plan_paid_until": "2027-01-04T10:00:00Z",
"addon_paid_until": null,
"payments_enabled": true
}Errors
| Status | Code | When |
|---|---|---|
| 403 | session_required | The request used an API key. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/billing/orders \
-H "Authorization: Bearer $FURL_TOKEN"Links
List links
/linksAuth: Session cookie, login token or API key (details)
Your links, newest first.
Query parameters
| Name | Type | Description |
|---|---|---|
| limit | integer | Items per page. Default 50, max 200. |
| cursor | string | The previous response’s next_cursor. |
| q | string | Search the short code and destination URL. |
Response
{
"items": [
{
"id": 42,
"short_code": "launch",
"short_url": "https://furl.io/launch",
"original_url": "https://example.com/spring-sale?utm_source=newsletter",
"is_custom": true,
"status": "active",
"http_status_code": 302,
"google_analytics_id": "G-ABC123XYZ9",
"has_ga_secret": true,
"webhook_url": "https://hooks.example.com/furl",
"webhook_secret": "whsec_9f2c4e1a7b3d5f60a8c2e4b6d8f0a1c3",
"expires_at": null,
"max_clicks": null,
"clicks_used": 0,
"created_at": "2026-10-01T09:30:00Z",
"updated_at": "2026-10-05T12:00:00Z"
}
],
"next_cursor": null
}Errors
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Invalid cursor. |
Plus the common errors (including 401 unauthorized).
curl -s "https://furl.io/api/v1/links?limit=20&q=launch" \
-H "Authorization: Bearer $FURL_API_KEY"Create a link
/linksAuth: Session cookie, login token or API key (details)
JSON body
| Name | Type | Description |
|---|---|---|
| original_urlrequired | string | Destination. http:// or https://, up to 8,192 characters, no username/password, not furl.io itself. |
| custom_code | string | Uses one custom code from your plan’s allowance. 3–64 characters from A–Z a–z 0–9 _ - ., case-sensitive. Reserved words and blocklisted brands/domains are refused. Omit for a random 7-character code. |
| http_status_code | integer | 301, 302, 307 or 308. Default 302. |
| expires_at | RFC3339 | null | When the link stops redirecting (410 Gone afterwards). Must be in the future. Omit or send null and the link never expires. Needs a plan with link_expiry (Pro and higher). |
| max_clicks | integer | null | View limit, an integer from 1 to 100,000. After this many redirects the link returns 410 Gone. Link-preview bots and HEAD requests don’t count. Omit or send null for unlimited views. Needs a plan with link_expiry (Pro and higher). |
| google_analytics_id | string | GA4 measurement ID, like G-XXXXXXXXXX. Needs a plan with integrations (Pro and higher). |
| ga_api_secret | string | GA4 Measurement Protocol API secret (max 64 characters). Write-only; never returned. Needs a plan with integrations (Pro and higher). |
| webhook_url | string | Public https:// URL for click webhooks. A webhook_secret is generated automatically. Needs a plan with integrations (Pro and higher). |
Response
{
"id": 42,
"short_code": "launch",
"short_url": "https://furl.io/launch",
"original_url": "https://example.com/spring-sale?utm_source=newsletter",
"is_custom": true,
"status": "active",
"http_status_code": 302,
"google_analytics_id": "G-ABC123XYZ9",
"has_ga_secret": true,
"webhook_url": "https://hooks.example.com/furl",
"webhook_secret": "whsec_9f2c4e1a7b3d5f60a8c2e4b6d8f0a1c3",
"expires_at": null,
"max_clicks": null,
"clicks_used": 0,
"created_at": "2026-10-01T09:30:00Z",
"updated_at": "2026-10-05T12:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 403 | plan_limit_reached | You already have as many active links as your plan allows. |
| 403 | custom_code_limit | A custom_code was sent but your plan’s custom code allowance is used up. |
| 403 | code_blocked | The custom code is reserved or matches a blocked brand/domain. |
| 409 | code_taken | The custom code is already in use. |
| 422 | validation_error | Invalid URL, code, reserved code, redirect type, GA4 ID, webhook URL, an expiry in the past, or a max_clicks outside 1–100,000. |
| 403 | link_expiry_not_in_plan | expires_at or max_clicks was sent but your plan doesn’t include expiry dates and view limits. |
| 403 | integrations_not_in_plan | webhook_url, google_analytics_id or ga_api_secret was sent but your plan doesn’t include webhooks and GA4 forwarding. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/links \
-H "Authorization: Bearer $FURL_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"original_url": "https://example.com/spring-sale?utm_source=newsletter",
"custom_code": "launch",
"webhook_url": "https://hooks.example.com/furl"
}'Get a link
/links/{code}Auth: Session cookie, login token or API key (details)
Path parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | The link’s short code (case-sensitive). URL-encode it if it contains special characters. |
Response
{
"id": 42,
"short_code": "launch",
"short_url": "https://furl.io/launch",
"original_url": "https://example.com/spring-sale?utm_source=newsletter",
"is_custom": true,
"status": "active",
"http_status_code": 302,
"google_analytics_id": "G-ABC123XYZ9",
"has_ga_secret": true,
"webhook_url": "https://hooks.example.com/furl",
"webhook_secret": "whsec_9f2c4e1a7b3d5f60a8c2e4b6d8f0a1c3",
"expires_at": null,
"max_clicks": null,
"clicks_used": 0,
"created_at": "2026-10-01T09:30:00Z",
"updated_at": "2026-10-05T12:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such link, or it belongs to someone else. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/links/launch \
-H "Authorization: Bearer $FURL_API_KEY"Update a link
/links/{code}Auth: Session cookie, login token or API key (details)
Send only the fields you want to change. For optional settings, null clears them. The short code can’t be changed.
Path parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | The link’s short code (case-sensitive). URL-encode it if it contains special characters. |
JSON body
| Name | Type | Description |
|---|---|---|
| original_url | string | New destination (same rules as create). |
| status | string | active, paused or archived. Banned links can’t be changed by their owner. |
| http_status_code | integer | 301, 302, 307 or 308. |
| expires_at | RFC3339 | null | Must be in the future. null removes the expiry (the link never expires). Setting or changing it needs a plan with link_expiry; removing it works on every plan. |
| max_clicks | integer | null | New view limit (1–100,000). null removes the limit. clicks_used is kept, so raising the limit above it reopens a link that reached its limit. Setting or changing it needs a plan with link_expiry; removing it works on every plan. |
| google_analytics_id | string | null | null stops GA4 forwarding. A new or changed value needs a plan with integrations; removing it works on every plan. |
| ga_api_secret | string | null | null removes the saved secret. A new value needs a plan with integrations. |
| webhook_url | string | null | null removes the webhook. Setting it for the first time generates a webhook_secret. A new or changed value needs a plan with integrations; removing it works on every plan. |
Response
{
"id": 42,
"short_code": "launch",
"short_url": "https://furl.io/launch",
"original_url": "https://example.com/spring-sale?utm_source=newsletter",
"is_custom": true,
"status": "paused",
"http_status_code": 302,
"google_analytics_id": "G-ABC123XYZ9",
"has_ga_secret": true,
"webhook_url": "https://hooks.example.com/furl",
"webhook_secret": "whsec_9f2c4e1a7b3d5f60a8c2e4b6d8f0a1c3",
"expires_at": null,
"max_clicks": null,
"clicks_used": 0,
"created_at": "2026-10-01T09:30:00Z",
"updated_at": "2026-10-05T12:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such link, or it belongs to someone else. |
| 422 | validation_error | An invalid value (including an expiry in the past or a max_clicks outside 1–100,000), or a status change on a banned link. |
| 403 | plan_limit_reached | Setting status to active would take you over your plan’s active link limit. |
| 403 | link_expiry_not_in_plan | A new expires_at or max_clicks value on a plan without link_expiry. |
| 403 | integrations_not_in_plan | A new or changed webhook_url, google_analytics_id or ga_api_secret on a plan without integrations. |
Plus the common errors (including 401 unauthorized).
curl -s -X PATCH https://furl.io/api/v1/links/launch \
-H "Authorization: Bearer $FURL_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"status":"paused","expires_at":null}'Delete a link
/links/{code}Auth: Session cookie, login token or API key (details)
Permanently deletes the link. It stops redirecting immediately.
Path parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | The link’s short code (case-sensitive). URL-encode it if it contains special characters. |
Response
204 No Content with an empty body.
Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such link, or it belongs to someone else. |
Plus the common errors (including 401 unauthorized).
curl -s -X DELETE https://furl.io/api/v1/links/launch \
-H "Authorization: Bearer $FURL_API_KEY"Rotate the webhook secret
/links/{code}/webhook/rotate-secretAuth: Session cookie, login token or API key (details)
Generates a new webhook_secret and returns the updated link. The old secret stops working immediately. Works on every plan while the link has a webhook.
Path parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | The link’s short code (case-sensitive). URL-encode it if it contains special characters. |
Response
{
"id": 42,
"short_code": "launch",
"short_url": "https://furl.io/launch",
"original_url": "https://example.com/spring-sale?utm_source=newsletter",
"is_custom": true,
"status": "active",
"http_status_code": 302,
"google_analytics_id": "G-ABC123XYZ9",
"has_ga_secret": true,
"webhook_url": "https://hooks.example.com/furl",
"webhook_secret": "whsec_9f2c4e1a7b3d5f60a8c2e4b6d8f0a1c3",
"expires_at": null,
"max_clicks": null,
"clicks_used": 0,
"created_at": "2026-10-01T09:30:00Z",
"updated_at": "2026-10-05T12:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such link, or it belongs to someone else. |
| 422 | validation_error | The link has no webhook configured. |
Plus the common errors (including 401 unauthorized).
curl -s -X POST https://furl.io/api/v1/links/launch/webhook/rotate-secret \
-H "Authorization: Bearer $FURL_API_KEY"Analytics
All ranges use RFC3339 timestamps. Unique visitors are distinct IP addresses within the range. In breakdowns, an empty value means unknown (or, for referrer_domain, direct traffic).
Click time series
/links/{code}/statsAuth: Session cookie, login token or API key (details)
Path parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | The link’s short code (case-sensitive). URL-encode it if it contains special characters. |
Query parameters
| Name | Type | Description |
|---|---|---|
| from | RFC3339 | Start of the range. Default: 7 days before to. |
| to | RFC3339 | End of the range. Default: now. The range may not exceed 366 days. |
| interval | string | hour or day. Defaults to hour if the range is 72 hours or less, else day. Hourly supports at most 31 days. |
Response
{
"total_clicks": 1284,
"unique_visitors": 903,
"series": [
{ "t": "2026-10-04T00:00:00Z", "clicks": 611, "unique": 455 },
{ "t": "2026-10-05T00:00:00Z", "clicks": 673, "unique": 448 }
]
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such link, or it belongs to someone else. |
| 400 | bad_request | Invalid from/to (not RFC3339, from after to, or more than 366 days). |
| 400 | bad_request | Invalid interval, or hour over more than 31 days. |
Plus the common errors (including 401 unauthorized).
curl -s "https://furl.io/api/v1/links/launch/stats?from=2026-09-28T00:00:00Z&to=2026-10-05T00:00:00Z&interval=day" \
-H "Authorization: Bearer $FURL_API_KEY"Breakdown by dimension
/links/{code}/stats/breakdownAuth: Session cookie, login token or API key (details)
Path parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | The link’s short code (case-sensitive). URL-encode it if it contains special characters. |
Query parameters
| Name | Type | Description |
|---|---|---|
| dimensionrequired | string | country, city, referrer_domain, device_type, os or browser. |
| from | RFC3339 | Start of the range. Default: 7 days before to. |
| to | RFC3339 | End of the range. Default: now. The range may not exceed 366 days. |
| limit | integer | Number of values. Default 10, max 100. |
Response
{
"dimension": "country",
"items": [
{ "value": "DE", "clicks": 512 },
{ "value": "US", "clicks": 301 },
{ "value": "", "clicks": 12 }
]
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such link, or it belongs to someone else. |
| 400 | bad_request | Invalid from/to (not RFC3339, from after to, or more than 366 days). |
| 400 | bad_request | Unknown dimension. |
Plus the common errors (including 401 unauthorized).
curl -s "https://furl.io/api/v1/links/launch/stats/breakdown?dimension=referrer_domain&limit=20" \
-H "Authorization: Bearer $FURL_API_KEY"Live view
/links/{code}/stats/liveAuth: Session cookie, login token or API key (details)
Per-minute clicks for the last 60 minutes and the 25 most recent clicks. A click appears within about 1–2 seconds. The dashboard polls this every 5 seconds.
Path parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | The link’s short code (case-sensitive). URL-encode it if it contains special characters. |
Response
{
"last_60m": [
{ "t": "2026-10-05T12:33:00Z", "clicks": 4 },
{ "t": "2026-10-05T12:34:00Z", "clicks": 7 }
],
"recent": [
{
"click_time": "2026-10-05T12:34:56.789Z",
"country": "DE",
"city": "Berlin",
"referrer_domain": "news.ycombinator.com",
"device_type": "desktop",
"os": "macOS",
"browser": "Safari"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such link, or it belongs to someone else. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/links/launch/stats/live \
-H "Authorization: Bearer $FURL_API_KEY"Webhook delivery log
/links/{code}/webhooksAuth: Session cookie, login token or API key (details)
Recent delivery attempts, newest first. Each retry is a separate item. See Webhooks.
Path parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | The link’s short code (case-sensitive). URL-encode it if it contains special characters. |
Query parameters
| Name | Type | Description |
|---|---|---|
| limit | integer | Items to return. Default 50, max 200. |
Response
{
"items": [
{
"id": 981,
"response_code": 200,
"duration_ms": 143,
"attempt": 1,
"status": "delivered",
"created_at": "2026-10-05T12:34:57Z",
"payload": {
"id": "dlv_4b7dd7351ffd59362e86a9e5",
"event": "link.clicked",
"link": { "id": 42, "short_code": "launch" },
"click": { "timestamp": "2026-10-05T12:34:56.789Z", "country": "DE", "city": "Berlin" }
}
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such link, or it belongs to someone else. |
Plus the common errors (including 401 unauthorized).
curl -s "https://furl.io/api/v1/links/launch/webhooks?limit=10" \
-H "Authorization: Bearer $FURL_API_KEY"Account overview
/stats/overviewAuth: Session cookie, login token or API key (details)
Totals across all of your links, with a daily click series and your top links.
Query parameters
| Name | Type | Description |
|---|---|---|
| from | RFC3339 | Start of the range. Default: 7 days before to. |
| to | RFC3339 | End of the range. Default: now. The range may not exceed 366 days. |
Response
{
"total_links": 12,
"total_clicks": 4810,
"series": [
{ "t": "2026-10-04T00:00:00Z", "clicks": 702 },
{ "t": "2026-10-05T00:00:00Z", "clicks": 655 }
],
"top_links": [
{ "short_code": "launch", "clicks": 2210 },
{ "short_code": "aB9xK2q", "clicks": 940 }
]
}Errors
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Invalid from/to (not RFC3339, from after to, or more than 366 days). |
Plus the common errors (including 401 unauthorized).
curl -s "https://furl.io/api/v1/stats/overview?from=2026-09-05T00:00:00Z" \
-H "Authorization: Bearer $FURL_API_KEY"API keys
API keys belong to a workspace: a key created while a workspace is selected only works with that workspace’s links. Managing keys needs the admin or owner role (403 insufficient_role for members).
List API keys
/api-keysAuth: Session cookie, login token or API key (details)
The workspace’s keys. Only the prefix is returned, never the full key.
Response
{
"items": [
{
"id": "b7e1d0c2-4f3a-4e8b-9c6d-1a2b3c4d5e6f",
"name": "ci-deploy",
"key_prefix": "furl_k3Jd9",
"is_active": true,
"created_at": "2026-10-02T08:00:00Z",
"last_used_at": "2026-10-05T11:58:12Z"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/api-keys \
-H "Authorization: Bearer $FURL_API_KEY"Create an API key
/api-keysAuth: Session cookie, login token or API key (details)
The full key is returned in key only in this response. Store it securely.
JSON body
| Name | Type | Description |
|---|---|---|
| namerequired | string | A label for the key, up to 100 characters. |
Response
{
"api_key": {
"id": "b7e1d0c2-4f3a-4e8b-9c6d-1a2b3c4d5e6f",
"name": "ci-deploy",
"key_prefix": "furl_k3Jd9",
"is_active": true,
"created_at": "2026-10-02T08:00:00Z",
"last_used_at": "2026-10-05T11:58:12Z"
},
"key": "furl_k3Jd9QwX2mLp7RtY4vBn8ZcF1hGs6KaE0uWo5NiT"
}Errors
| Status | Code | When |
|---|---|---|
| 422 | validation_error | Name longer than 100 characters. |
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/api-keys \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"ci-deploy"}'Revoke an API key
/api-keys/{id}Auth: Session cookie, login token or API key (details)
Revokes the key immediately.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | The key’s id. |
Response
204 No Content with an empty body.
Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No such key in this workspace. |
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
Plus the common errors (including 401 unauthorized).
curl -s -X DELETE https://furl.io/api/v1/api-keys/b7e1d0c2-4f3a-4e8b-9c6d-1a2b3c4d5e6f \
-H "Authorization: Bearer $FURL_TOKEN"Workspaces
Endpoints under /workspace act on the current workspace. Seats (members plus open invitations) and custom domains are limited by the workspace’s plan (max_members, max_domains).
List your workspaces
/workspacesAuth: Session cookie, login token or API key (details)
Every workspace you belong to, personal first, with your role in each.
Response
{
"items": [
{
"id": "5c2e8f0a-7d1b-4a3c-9e6f-2b4d6f8a0c1e",
"name": "Personal",
"slug": "personal-3f6c2a9e",
"personal": true,
"role": "owner",
"tier": "free",
"managed": false,
"custom_code_addon": 0,
"plan_paid_until": null,
"created_at": "2026-10-01T09:00:00Z"
},
{
"id": "9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a",
"name": "Acme Marketing",
"slug": "acme-marketing",
"personal": false,
"role": "owner",
"tier": "pro",
"managed": false,
"custom_code_addon": 0,
"plan_paid_until": "2027-01-04T10:00:00Z",
"created_at": "2026-10-02T08:00:00Z"
}
]
}Errors
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/workspaces \
-H "Authorization: Bearer $FURL_TOKEN"Create a team workspace
/workspacesAuth: Session cookie or login token only. API keys are refused (403 session_required).
Creates a team workspace on the Free plan, with you as its owner. You can own up to 10.
JSON body
| Name | Type | Description |
|---|---|---|
| namerequired | string | 1–64 characters. |
Response
{
"id": "9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a",
"name": "Acme Marketing",
"slug": "acme-marketing",
"personal": false,
"role": "owner",
"tier": "free",
"managed": false,
"custom_code_addon": 0,
"plan_paid_until": null,
"created_at": "2026-10-02T08:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 422 | validation_error | Missing or too long name, or you already own 10 team workspaces. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/workspaces \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"Acme Marketing"}'Current workspace
/workspaceAuth: Session cookie, login token or API key (details)
The current workspace with its plan and usage (as on /me).
Response
{
"workspace": {
"id": "9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a",
"name": "Acme Marketing",
"slug": "acme-marketing",
"personal": false,
"role": "owner",
"tier": "pro",
"managed": false,
"custom_code_addon": 0,
"plan_paid_until": "2027-01-04T10:00:00Z",
"created_at": "2026-10-02T08:00:00Z"
},
"plan": { "code": "pro", "…": "…" },
"usage": { "active_links": 42, "custom_codes": 3, "custom_code_addon": 0 }
}Errors
| Status | Code | When |
|---|---|---|
| 403 | not_member | X-Furl-Workspace names a workspace you don’t belong to. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/workspace \
-H "Authorization: Bearer $FURL_TOKEN" \
-H "X-Furl-Workspace: 9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a"Rename the workspace
/workspaceAuth: Session cookie, login token or API key (details). Needs the admin or owner role in the current workspace.
JSON body
| Name | Type | Description |
|---|---|---|
| namerequired | string | 1–64 characters. |
Response
{
"id": "9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a",
"name": "Acme Marketing",
"slug": "acme-marketing",
"personal": false,
"role": "owner",
"tier": "pro",
"managed": false,
"custom_code_addon": 0,
"plan_paid_until": "2027-01-04T10:00:00Z",
"created_at": "2026-10-02T08:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 409 | personal_workspace | Not possible in a personal workspace. |
| 422 | validation_error | Missing or too long name. |
Plus the common errors (including 401 unauthorized).
curl -s -X PATCH https://furl.io/api/v1/workspace \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"Acme Growth"}'Delete the workspace
/workspaceAuth: Session cookie or login token only; the owner of a team workspace.
Permanently deletes the team workspace with all its links (they stop redirecting immediately), their analytics, API keys, invitations and custom domains.
Response
204 No Content with an empty body.
Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 409 | personal_workspace | Not possible in a personal workspace. |
| 403 | session_required | The request used an API key. |
Plus the common errors (including 401 unauthorized).
curl -s -X DELETE https://furl.io/api/v1/workspace \
-H "Authorization: Bearer $FURL_TOKEN" \
-H "X-Furl-Workspace: 9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a"List members
/workspace/membersAuth: Session cookie, login token or API key (details)
Response
{
"items": [
{
"user_id": "3f6c2a9e-5b1d-4c47-9a2e-7d0b8f1e4a63",
"email": "[email protected]",
"role": "owner",
"created_at": "2026-10-02T08:00:00Z"
}
]
}Errors
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/workspace/members \
-H "Authorization: Bearer $FURL_TOKEN"Change a member’s role
/workspace/members/{userID}Auth: Session cookie, login token or API key (details). Needs the admin or owner role in the current workspace.
Sets the role to member or admin. Only the owner can set owner, which transfers ownership: the previous owner becomes an admin. Returns the updated member list.
Path parameters
| Name | Type | Description |
|---|---|---|
| userIDrequired | string | The member’s user_id. |
JSON body
| Name | Type | Description |
|---|---|---|
| rolerequired | string | member, admin or owner. |
Response
{
"items": [
{
"user_id": "3f6c2a9e-5b1d-4c47-9a2e-7d0b8f1e4a63",
"email": "[email protected]",
"role": "owner",
"created_at": "2026-10-02T08:00:00Z"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 409 | owner_required | The workspace must keep exactly one owner. |
| 404 | not_found | No such member. |
Plus the common errors (including 401 unauthorized).
curl -s -X PATCH https://furl.io/api/v1/workspace/members/3f6c2a9e-5b1d-4c47-9a2e-7d0b8f1e4a63 \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"role":"admin"}'Remove a member or leave
/workspace/members/{userID}Auth: Session cookie, login token or API key (details). Removing someone else needs the admin or owner role; anyone can remove themselves (leave).
The links the member created stay in the workspace. The owner can’t be removed.
Path parameters
| Name | Type | Description |
|---|---|---|
| userIDrequired | string | The member’s user_id (yours to leave). |
Response
204 No Content with an empty body.
Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 409 | owner_required | The owner can’t leave or be removed; transfer ownership first. |
| 404 | not_found | No such member. |
Plus the common errors (including 401 unauthorized).
curl -s -X DELETE https://furl.io/api/v1/workspace/members/3f6c2a9e-5b1d-4c47-9a2e-7d0b8f1e4a63 \
-H "Authorization: Bearer $FURL_TOKEN"List open invitations
/workspace/invitesAuth: Session cookie, login token or API key (details). Needs the admin or owner role in the current workspace.
Response
{
"items": [
{
"id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
"email": "[email protected]",
"role": "member",
"expires_at": "2026-10-13T08:00:00Z",
"created_at": "2026-10-06T08:00:00Z"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/workspace/invites \
-H "Authorization: Bearer $FURL_TOKEN"Invite someone
/workspace/invitesAuth: Session cookie, login token or API key (details). Needs the admin or owner role in the current workspace.
Emails an invitation link that works for 7 days. The invited person accepts it while signed in with that email address (see accept an invitation). An open invitation uses a seat.
JSON body
| Name | Type | Description |
|---|---|---|
| emailrequired | string | The address to invite. |
| rolerequired | string | member or admin. |
Response
{
"id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
"email": "[email protected]",
"role": "member",
"expires_at": "2026-10-13T08:00:00Z",
"created_at": "2026-10-06T08:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 403 | member_limit | Every seat on the workspace’s plan is taken (members plus open invitations). |
| 409 | already_member | That person is already a member. |
| 409 | personal_workspace | Not possible in a personal workspace. |
| 422 | validation_error | Invalid email address or role. |
| 503 | email_unavailable | The invitation email couldn’t be sent. Try again later. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/workspace/invites \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","role":"member"}'Withdraw an invitation
/workspace/invites/{id}Auth: Session cookie, login token or API key (details). Needs the admin or owner role in the current workspace.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | The invitation’s id. |
Response
204 No Content with an empty body.
Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 404 | not_found | No such invitation. |
Plus the common errors (including 401 unauthorized).
curl -s -X DELETE https://furl.io/api/v1/workspace/invites/e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b \
-H "Authorization: Bearer $FURL_TOKEN"Accept an invitation
/invites/acceptAuth: Session cookie or login token only. API keys are refused (403 session_required).
Joins the workspace with the role in the invitation. The signed-in account’s email must be the invited address. Returns the workspace you joined.
JSON body
| Name | Type | Description |
|---|---|---|
| tokenrequired | string | The token from the invitation link. |
Response
{
"id": "9a4d7c1e-2b3f-4e5a-8c6d-0f1e2d3c4b5a",
"name": "Acme Marketing",
"slug": "acme-marketing",
"personal": false,
"role": "member",
"tier": "pro",
"managed": false,
"custom_code_addon": 0,
"plan_paid_until": "2027-01-04T10:00:00Z",
"created_at": "2026-10-02T08:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 404 | invite_invalid | Unknown, expired, withdrawn or already used invitation. |
| 403 | invite_email_mismatch | The invitation was sent to a different email address. |
| 403 | session_required | The request used an API key. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/invites/accept \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"token":"…"}'List custom domains
/workspace/domainsAuth: Session cookie, login token or API key (details)
status is pending, active (verified), failed or disabled. To verify a domain, create a DNS TXT record named verification_name with the value verification_token. Verified domains will soon be usable for short links.
Response
{
"items": [
{
"id": "0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a",
"hostname": "go.example.com",
"status": "pending",
"verification_name": "_furl-verify.go.example.com",
"verification_token": "furl-verify=3q9TzX1mK8pLw2Rv",
"verified_at": null,
"last_checked_at": null,
"last_error": null,
"created_at": "2026-10-06T08:00:00Z"
}
]
}Errors
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/workspace/domains \
-H "Authorization: Bearer $FURL_TOKEN"Add a custom domain
/workspace/domainsAuth: Session cookie, login token or API key (details). Needs the admin or owner role in the current workspace.
JSON body
| Name | Type | Description |
|---|---|---|
| hostnamerequired | string | A domain you control, e.g. go.example.com. |
Response
{
"id": "0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a",
"hostname": "go.example.com",
"status": "pending",
"verification_name": "_furl-verify.go.example.com",
"verification_token": "furl-verify=3q9TzX1mK8pLw2Rv",
"verified_at": null,
"last_checked_at": null,
"last_error": null,
"created_at": "2026-10-06T08:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 403 | domain_limit | The workspace’s plan doesn’t include (more) custom domains. |
| 409 | domain_taken | The domain is already added to a workspace. |
| 422 | validation_error | Not a valid hostname, or a furl.io domain. |
Plus the common errors (including 401 unauthorized).
curl -s https://furl.io/api/v1/workspace/domains \
-H "Authorization: Bearer $FURL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"hostname":"go.example.com"}'Verify a custom domain
/workspace/domains/{id}/verifyAuth: Session cookie, login token or API key (details). Needs the admin or owner role in the current workspace.
Looks up the TXT record now. The domain becomes active when the record is found; otherwise its status is failed with last_error. DNS changes can take a few minutes to show up.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | The domain’s id. |
Response
{
"id": "0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a",
"hostname": "go.example.com",
"status": "active",
"verification_name": "_furl-verify.go.example.com",
"verification_token": "furl-verify=3q9TzX1mK8pLw2Rv",
"verified_at": "2026-10-06T08:05:00Z",
"last_checked_at": "2026-10-06T08:05:00Z",
"last_error": null,
"created_at": "2026-10-06T08:00:00Z"
}Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 404 | not_found | No such domain in this workspace. |
Plus the common errors (including 401 unauthorized).
curl -s -X POST https://furl.io/api/v1/workspace/domains/0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a/verify \
-H "Authorization: Bearer $FURL_TOKEN"Remove a custom domain
/workspace/domains/{id}Auth: Session cookie, login token or API key (details). Needs the admin or owner role in the current workspace.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | The domain’s id. |
Response
204 No Content with an empty body.
Errors
| Status | Code | When |
|---|---|---|
| 403 | insufficient_role | Your role in the workspace doesn’t allow this. |
| 409 | domain_in_use | Links still use the domain. |
| 404 | not_found | No such domain in this workspace. |
Plus the common errors (including 401 unauthorized).
curl -s -X DELETE https://furl.io/api/v1/workspace/domains/0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a \
-H "Authorization: Bearer $FURL_TOKEN"