Links
All link endpoints are scoped to a workspace: /api/workspaces/:wsId/links.
POST /workspaces/:wsId/links
Create a short link. Auto-generates a slug if not provided.
Role: editor+
curl -X POST https://nobsredir.com/api/workspaces/ws_abc123/links \ -H "X-API-Key: nobs_your_key" \ -H "Content-Type: application/json" \ -d '{ "target": "https://example.com/my-long-page", "slug": "demo", "domain": "go.yourco.com", "title": "Demo page", "utm_params": {"source": "newsletter", "medium": "email"}, "expires_at": "2025-12-31T23:59:59Z", "fallback_url": "https://example.com/expired", "routing_rules": { "geo": {"DE": "https://example.de/seite", "FR": "https://example.fr/page"}, "device": {"mobile": "https://m.example.com/page"}, "os": {"iOS": "https://apps.apple.com/app/example", "Android": "https://play.google.com/store/apps/details?id=com.example"} }, "rules": [ { "name": "Germany", "conditions": [{"field": "country", "operator": "eq", "value": "DE"}], "action": {"type": "redirect", "url": "https://example.de/seite"} } ], "og_title": "Check this out", "og_description": "A great page worth visiting", "og_image": "https://example.com/og.png", "tags": ["marketing", "q1-launch"], "note": "Internal: approved by legal on Jan 15", "ab_variants": [ {"name": "Control", "url": "https://example.com/page-v1", "weight": 60}, {"name": "Variant A", "url": "https://example.com/page-v2", "weight": 40} ], "password": "launch2025", "deep_link": { "ios_app_url": "myapp://promo/123", "ios_store_url": "https://apps.apple.com/app/example/id123", "android_app_url": "myapp://promo/123", "android_store_url": "https://play.google.com/store/apps/details?id=com.example", "interstitial": true } }'Body:
| Field | Type | Required | Description |
|---|---|---|---|
target | string | yes | Destination URL. Must be a valid URL. |
slug | string | no | Custom slug. Letters, numbers, hyphens, underscores only. Auto-generated if omitted. Requires a custom domain - the default domain (fnl.sh) always uses auto-generated slugs. |
domain | string | no | Domain for the short link. Defaults to fnl.sh. |
title | string | no | Display name in the dashboard. |
tags | string[] | no | Tags for organizing links. Lowercase letters, numbers, hyphens, and underscores ([a-z0-9_-]), max 32 chars each, max 10 per link. |
note | string | no | Internal note. Visible only to workspace members - never leaked to visitors or in redirects. |
utm_params | object | no | UTM parameters: {source?, medium?, campaign?, term?, content?}. Appended to target URL on redirect. |
expires_at | string | no | ISO 8601 expiration date. After this, returns 410 or redirects to fallback_url. |
fallback_url | string | no | Redirect target after expiration. Must be a valid URL. If not set, the workspace-level expired_page_url is used as a fallback (configure in workspace settings via PATCH /workspaces/:wsId). |
routing_rules | object | no | Geo/device/OS routing. Requires Team or Agency plan. {geo?: {country_code: url}, device?: {mobile|desktop|tablet: url}, os?: {iOS|Android|Windows|macOS|Linux: url}}. |
ab_variants | array | no | A/B split testing. Requires Team or Agency plan. Array of 2-4 variants: [{name: string, url: string, weight: integer}]. Weights must be 1-99 and sum to 100. Variant names must be unique. If set, routing rules take priority - A/B applies only when no routing rule matches. |
og_title | string | no | Open Graph title for social previews. |
og_description | string | no | Open Graph description for social previews. |
og_image | string | no | Open Graph image URL for social previews. Must be a valid URL. |
password | string | no | Password-protect the link. Must be at least 8 characters. Visitors see a branded password form before being redirected. The password is never stored or returned in API responses. |
rules | array | no | Conditional routing rules (Pro+). Ordered IF/THEN rules evaluated at redirect time - first match wins. See Rules engine for full field/operator reference. |
template_id | string | no | Apply a link template’s config as defaults. The template must belong to the same workspace. Any field set on the link overrides the template. Requires Pro plan or higher. See Templates API. |
interstitial | object | no | Show a custom page before redirecting. Requires Pro plan or higher. See Interstitials below. |
sequence | object | no | Redirect to a different URL on each visit. Requires Pro plan or higher. See Sequences below. |
redirect_type | integer | no | HTTP redirect status: 301 (permanent) or 302 (temporary, default). Use 301 to pass SEO link equity to the destination. Use 302 for campaigns, temporary links, or when the destination may change. |
deep_link | object | no | Mobile deep linking configuration. Requires Pro plan or higher. See Deep Links below. |
Response 201:
{ "id": "lnk_xyz789", "domain": "go.yourco.com", "slug": "demo", "target": "https://example.com/my-long-page", "title": "Demo page", "utm_params": {"source": "newsletter", "medium": "email"}, "expires_at": "2025-12-31T23:59:59Z", "fallback_url": "https://example.com/expired", "routing_rules": {"geo": {"DE": "https://example.de/seite"}, "device": {"mobile": "https://m.example.com/page"}, "os": {"iOS": "https://apps.apple.com/app/example"}}, "rules": [{"name": "Germany", "conditions": [{"field": "country", "operator": "eq", "value": "DE"}], "action": {"type": "redirect", "url": "https://example.de/seite"}}], "og_title": "Check this out", "og_description": "A great page worth visiting", "og_image": "https://example.com/og.png", "tags": ["marketing", "q1-launch"], "note": "Internal: approved by legal on Jan 15", "ab_variants": [ {"name": "Control", "url": "https://example.com/page-v1", "weight": 60}, {"name": "Variant A", "url": "https://example.com/page-v2", "weight": 40} ], "has_password": true, "redirect_type": 302, "deep_link": { "ios_app_url": "myapp://promo/123", "ios_store_url": "https://apps.apple.com/app/example/id123", "android_app_url": "myapp://promo/123", "android_store_url": "https://play.google.com/store/apps/details?id=com.example", "interstitial": true }, "short_url": "https://go.yourco.com/demo"}Errors:
400- Invalid target URL, slug format,expires_at,fallback_url, orog_image400- Custom slug provided without a custom domain (default domain uses auto-generated slugs)400- More than 10 tags400-passwordless than 8 characters400-redirect_typeis not301or302400-ab_variantsinvalid (fewer than 2, more than 4, weights don’t sum to 100, duplicate names, invalid URLs, weights not 1-99)402- Routing rules or A/B variants require a Team or Agency plan402- Rules engine requires a Pro plan or higher402- Interstitials require a Pro plan or higher402- Sequences require a Pro plan or higher402- Deep links require a Pro plan or higher400- Invalid rules (bad field, operator, field/operator mismatch, invalid action URL, too many rules for plan)409- Custom slug already exists on this domain429- Rate limit exceeded (max 30 requests per 10 seconds per workspace)503- Temporary error, try again
GET /workspaces/:wsId/links
List links with pagination and filtering.
Role: viewer+
curl "https://nobsredir.com/api/workspaces/ws_abc123/links?page=1&limit=20&domain=fnl.sh&tag=marketing" \ -H "X-API-Key: nobs_your_key"Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number |
limit | int | 20 | Items per page |
domain | string | - | Filter by domain |
prefix | string | - | Filter by slug prefix (defaults to fnl.sh domain) |
tag | string | - | Filter by tag. Only links with this tag are returned. |
Response 200:
{ "links": [ { "id": "lnk_xyz789", "workspace_id": "ws_abc123", "domain": "fnl.sh", "slug": "a1b2c3", "target": "https://example.com/page", "title": null, "tags": ["marketing"], "note": null, "utm_params": null, "expires_at": null, "fallback_url": null, "routing_rules": null, "rules": null, "ab_variants": null, "og_title": null, "og_description": null, "og_image": null, "has_password": false, "created_by": "usr_abc123", "created_at": "2025-01-20T14:30:00.000Z", "updated_at": "2025-01-20T14:30:00.000Z", "click_count": 142 } ], "total": 1, "page": 1, "limit": 20}GET /workspaces/:wsId/links/tags
List all tags used in the workspace, with metadata.
Role: viewer+
curl https://nobsredir.com/api/workspaces/ws_abc123/links/tags \ -H "X-API-Key: nobs_your_key"Response 200:
{ "tags": [ { "name": "marketing", "color": "#ff0000", "description": "Marketing campaign links", "date_from": "2026-01-01", "date_to": "2026-03-31" }, { "name": "product", "color": "", "description": "", "date_from": null, "date_to": null } ]}Tags are returned sorted alphabetically. Only tags currently in use (assigned to at least one link) are included.
PUT /workspaces/:wsId/links/tags/:tagName
Create or update tag metadata. The tag name must be lowercase letters, numbers, hyphens, and underscores ([a-z0-9_-]).
Role: editor+
curl -X PUT https://nobsredir.com/api/workspaces/ws_abc123/links/tags/black-friday \ -H "X-API-Key: nobs_your_key" \ -H "Content-Type: application/json" \ -d '{ "color": "#ff0000", "description": "Black Friday 2026 campaign", "date_from": "2026-11-25", "date_to": "2026-11-30" }'Body:
| Field | Type | Description |
|---|---|---|
color | string | Hex color (e.g. #ff0000) or empty string to clear |
description | string | Description, max 500 characters |
date_from | string | Campaign start date (YYYY-MM-DD) or null |
date_to | string | Campaign end date (YYYY-MM-DD) or null |
All fields are optional. Only provided fields are updated. Each workspace can have up to 100 tags.
Response 200:
{"ok": true}Errors: 402 - Tag limit reached (max 100 per workspace).
Errors: 400 - Invalid color format, date format, or description too long. 403 - Requires editor+ role.
DELETE /workspaces/:wsId/links/tags/:tagName
Delete a tag. Removes the tag metadata and unlinks it from all links in the workspace.
Role: editor+
curl -X DELETE https://nobsredir.com/api/workspaces/ws_abc123/links/tags/old-campaign \ -H "X-API-Key: nobs_your_key"Response 200:
{"ok": true}GET /workspaces/:wsId/links/:linkId
Get a single link by ID.
Role: viewer+
curl https://nobsredir.com/api/workspaces/ws_abc123/links/lnk_xyz789 \ -H "X-API-Key: nobs_your_key"Response 200: Same shape as a single item in the list response, including click_count, tags, note, has_password, ab_variants, rules, interstitial, and sequence.
Errors: 404 - Link not found.
PATCH /workspaces/:wsId/links/:linkId
Update a link. Only send the fields you want to change. Set a field to null to clear it.
Role: editor+
curl -X PATCH https://nobsredir.com/api/workspaces/ws_abc123/links/lnk_xyz789 \ -H "X-API-Key: nobs_your_key" \ -H "Content-Type: application/json" \ -d '{"tags": ["marketing", "updated"], "note": "Reviewed by team"}'Body: Same fields as create, all optional. Send null to clear optional fields like tags, note, utm_params, expires_at, routing_rules, ab_variants, rules, password, interstitial, sequence, redirect_type, etc.
You can update any combination of fields in a single call - change the target, add tags, update the note, all at once. Changes take effect immediately.
Set password to a new string to change the password, or null to remove it. Set ab_variants to null to remove A/B testing. Set redirect_type to 301 or 302, or null to reset to the default (302).
Response 200:
{"ok": true}Errors:
400- Invalid field values, no updates provided400- Cannot set custom slug on default domain (fnl.sh)400- More than 10 tags404- Link not found409- Slug already exists on this domain
DELETE /workspaces/:wsId/links/:linkId
Delete a link. Returns 404 for future redirects.
Role: admin+
curl -X DELETE https://nobsredir.com/api/workspaces/ws_abc123/links/lnk_xyz789 \ -H "X-API-Key: nobs_your_key"Response 200:
{"ok": true}Errors: 404 - Link not found.
POST /workspaces/:wsId/links/bulk
Bulk create up to 100 links. Links with invalid target URLs, duplicate custom slugs, or custom slugs on the default domain (fnl.sh) are silently skipped.
Role: editor+
curl -X POST https://nobsredir.com/api/workspaces/ws_abc123/links/bulk \ -H "X-API-Key: nobs_your_key" \ -H "Content-Type: application/json" \ -d '{ "links": [ {"target": "https://example.com/page-1", "slug": "p1"}, {"target": "https://example.com/page-2", "slug": "p2"}, {"target": "https://example.com/page-3"} ] }'Body:
| Field | Type | Required | Description |
|---|---|---|---|
links | array | yes | Array of link objects (same fields as single create). Max 100. |
Response 201:
{ "created": [ {"id": "lnk_a1", "domain": "fnl.sh", "slug": "p1", "target": "https://example.com/page-1", "title": null}, {"id": "lnk_a2", "domain": "fnl.sh", "slug": "p2", "target": "https://example.com/page-2", "title": null}, {"id": "lnk_a3", "domain": "fnl.sh", "slug": "x7k9m2", "target": "https://example.com/page-3", "title": null} ], "count": 3}Errors:
400- links array empty or exceeds 100 items429- Rate limit exceeded (max 5 bulk requests per 10 seconds per workspace)
DELETE /workspaces/:wsId/links/bulk
Bulk delete up to 100 links by ID.
Role: admin+
curl -X DELETE https://nobsredir.com/api/workspaces/ws_abc123/links/bulk \ -H "X-API-Key: nobs_your_key" \ -H "Content-Type: application/json" \ -d '{"ids": ["lnk_a1", "lnk_a2", "lnk_a3"]}'Body:
| Field | Type | Required | Description |
|---|---|---|---|
ids | string[] | yes | Array of link IDs. Max 100. |
Response 200:
{"deleted": 3}Non-existent IDs are silently ignored. deleted reflects how many were actually removed.
Rules Engine (Pro+)
Set up conditional routing with IF/THEN rules. Each rule has a name, an array of conditions (AND logic), and an action. Rules are evaluated top-to-bottom at redirect time - first match wins. If no rule matches, the link’s default target is used.
Set the rules array on create or update. Set to null on PATCH to clear all rules.
Rule object:
| Field | Type | Description |
|---|---|---|
name | string | Human-readable label (e.g. “German visitors”) |
conditions | array | Conditions that must ALL be true. Empty array = catch-all. |
action | object | {"type": "redirect", "url": "https://..."} |
Condition object:
| Field | Type | Description |
|---|---|---|
field | string | What to check. See available fields below. |
operator | string | How to compare: eq, neq, contains, starts_with, in, not_in, gt, lt, between |
value | string or string[] | Value(s) to match. Use array for in, not_in, between. |
Available fields by plan:
- Pro:
country,continent,device,os,browser - Team/Agency: all Pro fields plus
language,referrer,time,day,query,cookie,click_count
Max rules per plan: Pro = 5, Team = 20, Agency = 50.
Example:
{ "rules": [ { "name": "Germany", "conditions": [{"field": "country", "operator": "eq", "value": "DE"}], "action": {"type": "redirect", "url": "https://example.de/seite"} }, { "name": "Mobile users", "conditions": [{"field": "device", "operator": "eq", "value": "mobile"}], "action": {"type": "redirect", "url": "https://m.example.com"} } ]}See the full Rules engine guide for all operators, field/operator compatibility, and more examples.
Priority: Rules engine > legacy geo/device/OS routing > A/B testing > default target.
Performance: Zero latency added. All condition data comes from request headers and connection metadata already available at redirect time.
Interstitials (Pro+)
Show a custom page between the click and the redirect. Set the interstitial object on create or update. Set to null on PATCH to remove.
| Field | Type | Description |
|---|---|---|
title | string | Page heading (required, max 200 chars) |
body | string | Body text (max 500 chars) |
cta_text | string | Button text (max 50 chars, default: “Continue”) |
delay | integer | Auto-redirect delay in seconds (1-30, default: 5) |
show_once | boolean | Show once per visitor via cookie (default: true) |
style | object | Custom colors: bg_color, text_color, button_color (hex values) |
Example:
{ "interstitial": { "title": "Quick heads up", "body": "You are leaving our site. The destination is managed by a third party.", "cta_text": "Got it", "delay": 5, "show_once": true, "style": {"bg_color": "#0a0a0a", "text_color": "#e5e5e5", "button_color": "#3b82f6"} }}The interstitial page auto-redirects after the delay. Visitors can also click the CTA button to continue immediately. Social bots skip the interstitial entirely.
See the Interstitials guide for how interstitials interact with other features.
Sequences (Pro+)
Make a single link redirect to a different URL on each visit per visitor. Set the sequence object on create or update. Set to null on PATCH to remove.
| Field | Type | Description |
|---|---|---|
urls | array | Ordered list of 2-50 destinations. Each: {url: string, title?: string} |
loop | boolean | Restart from beginning after last URL (default: false - stays on last) |
Example:
{ "sequence": { "urls": [ {"url": "https://example.com/step-1", "title": "Welcome"}, {"url": "https://example.com/step-2", "title": "Setup"}, {"url": "https://example.com/step-3", "title": "Done"} ], "loop": false }}Step tracking uses a cookie per visitor per link. First visit serves URL 1, second visit URL 2, and so on.
With loop: false, the visitor stays on the last URL permanently. With loop: true, after the last URL they start back at URL 1.
Sequences resolve the target before routing rules, A/B testing, and other redirect logic. This means rules can still override the sequence-determined target.
See the Sequences guide for more details.
Deep Links (Pro+)
Configure mobile deep linking to open native apps when visitors tap your short link on iOS or Android. Set the deep_link object on create or update.
| Field | Type | Description |
|---|---|---|
ios_app_url | string | iOS URI scheme or Universal Link URL (e.g. myapp://promo/123) |
ios_store_url | string | Apple App Store URL (fallback if app not installed) |
ios_fallback_url | string | Web fallback URL for iOS |
android_app_url | string | Android URI scheme or App Link URL |
android_store_url | string | Google Play Store URL (fallback if app not installed) |
android_fallback_url | string | Web fallback URL for Android |
interstitial | boolean | Show an app detection page that tries to open the app, then falls back to the store or web URL automatically |
At least one of ios_app_url or android_app_url is required. Store and fallback URLs must be valid HTTP(S) URLs. App URLs can use custom URI schemes.
How it works:
- When a mobile visitor taps the link, the redirect engine detects their platform (iOS/Android) and applies the deep link config.
- With
interstitial: true, a lightweight page attempts to open the app via the URI scheme. If the app isn’t installed (detected by timeout), it redirects to the store URL or web fallback. - Without
interstitial, the visitor is redirected directly to the app URL (302). - Desktop visitors and social media bots are not affected - they get the normal redirect.
- Deep links run after routing rules. If a geo/device/OS routing rule resolves a different target, that resolved target becomes the web fallback for the deep link.
- Set
deep_linktonullin a PATCH request to remove deep linking from a link.
GET /workspaces/:wsId/links/:linkId/qr
Generate a QR code for a link. Returns the image directly (not JSON). Available on all plans.
Role: viewer+
curl https://nobsredir.com/api/workspaces/ws_abc123/links/LINK_ID/qr \ -H "X-API-Key: nobs_your_key" \ -o qr-code.svgQuery parameters:
| Param | Type | Default | Description |
|---|---|---|---|
format | string | svg | Output format: svg or png |
size | integer | 400 | Image dimensions in pixels (100-2000) |
fg | string | 000000 | Foreground color as 6-character hex (no # prefix) |
bg | string | ffffff | Background color as 6-character hex, or transparent (SVG only) |
margin | integer | 2 | Quiet zone around the QR code in modules (0-8) |
ec | string | M | Error correction level: L, M, Q, or H |
download | boolean | false | Set to 1 or true for a download attachment response |
Response: Raw image bytes with appropriate Content-Type header (image/svg+xml or image/png).
Examples:
# SVG with custom brand colorscurl "https://nobsredir.com/api/workspaces/ws_abc123/links/LINK_ID/qr?fg=1a1a2e&bg=e2e2e2" \ -H "X-API-Key: nobs_your_key" -o branded-qr.svg
# High-res PNG for printcurl "https://nobsredir.com/api/workspaces/ws_abc123/links/LINK_ID/qr?format=png&size=2000&ec=H" \ -H "X-API-Key: nobs_your_key" -o print-qr.png
# Transparent background SVGcurl "https://nobsredir.com/api/workspaces/ws_abc123/links/LINK_ID/qr?bg=transparent" \ -H "X-API-Key: nobs_your_key" -o transparent-qr.svgRate limit: 30 requests per minute per user.
Errors:
400- Invalid parameters (size out of range, bad color format, transparent with PNG)404- Link not found or wrong workspace