Surveys
Attach a one-question survey to any link. Visitors see the question before redirecting, tap an answer, and land on the destination. Responses are recorded with country, device, and referrer data. No Javascript involved - the survey page is pure HTML that loads instantly.
Plan: Available on all plans, including Free.
| Plan | Surveys | Responses/month |
|---|---|---|
| Free | 1 | 50 |
| Pro | Unlimited | 1,000 |
| Team | Unlimited | 10,000 |
| Agency | Unlimited | 100,000 |
When the monthly response limit is reached, surveys stop appearing on redirects until the next billing cycle. Existing responses are kept.
All survey endpoints are scoped to a link:
/api/workspaces/:wsId/links/:linkId/surveyPUT /workspaces/:wsId/links/:linkId/survey
Create or update a survey on a link. If the link already has a survey, it gets updated. If not, a new one is created.
Role: editor+
curl -X PUT https://nobsredir.com/api/workspaces/ws_abc123/links/lnk_xyz789/survey \ -H "X-API-Key: nobs_your_key" \ -H "Content-Type: application/json" \ -d '{ "question": "How did you find this?", "options": ["Social", "Email", "Search", "Friend"], "show_once": true, "style": { "bg_color": "#1a1a2e", "text_color": "#e5e5e5", "button_color": "#3b82f6" } }'Body:
| Field | Type | Required | Description |
|---|---|---|---|
question | string | yes | The question to ask. 1-200 characters. |
options | string[] | yes | 2-4 answer options. Each 1-50 characters. |
show_once | boolean | no | Only show the survey once per visitor via cookie. Default: true. |
style | object | no | Custom colors for the survey card. See Styling below. |
Response 201 (created) or 200 (updated):
{ "survey": { "id": "srv_a1b2c3d4e5f6", "question": "How did you find this?", "options": ["Social", "Email", "Search", "Friend"], "active": true, "show_once": true, "style": { "bg_color": "#1a1a2e", "text_color": "#e5e5e5", "button_color": "#3b82f6" }, "created_at": "2026-03-07T10:00:00.000Z", "updated_at": "2026-03-07T10:00:00.000Z" }}Errors:
400- Missing or invalid question, wrong number of options (need 2-4), option too long, empty option402- Survey limit reached for your plan, or surveys require a Pro plan404- Link not found or doesn’t belong to this workspace
GET /workspaces/:wsId/links/:linkId/survey
Get the survey configuration for a link, including total response count.
Role: viewer+
curl https://nobsredir.com/api/workspaces/ws_abc123/links/lnk_xyz789/survey \ -H "X-API-Key: nobs_your_key"Response 200 (survey exists):
{ "survey": { "id": "srv_a1b2c3d4e5f6", "question": "How did you find this?", "options": ["Social", "Email", "Search", "Friend"], "active": true, "show_once": true, "style": { "bg_color": "#1a1a2e", "text_color": "#e5e5e5", "button_color": "#3b82f6" }, "total_responses": 342, "created_at": "2026-03-07T10:00:00.000Z", "updated_at": "2026-03-07T10:00:00.000Z" }}Response 200 (no survey):
{ "survey": null}Errors: 404 - Link not found.
DELETE /workspaces/:wsId/links/:linkId/survey
Delete the survey from a link. This permanently removes the survey configuration and all recorded responses. The link reverts to a normal redirect.
Role: editor+
curl -X DELETE https://nobsredir.com/api/workspaces/ws_abc123/links/lnk_xyz789/survey \ -H "X-API-Key: nobs_your_key"Response 200:
{ "ok": true}Errors:
404- Link not found, or no survey exists on this link
GET /workspaces/:wsId/links/:linkId/survey/results
Get aggregated survey results with breakdowns by option, country, device, and day.
Role: viewer+
curl "https://nobsredir.com/api/workspaces/ws_abc123/links/lnk_xyz789/survey/results?days=30" \ -H "X-API-Key: nobs_your_key"Query params:
| Param | Type | Default | Description |
|---|---|---|---|
days | integer | 30 | Number of days to look back |
Response 200:
{ "question": "How did you find this?", "total_responses": 342, "options": [ { "index": 0, "label": "Social", "count": 145, "percent": 42.4 }, { "index": 1, "label": "Email", "count": 98, "percent": 28.7 }, { "index": 2, "label": "Search", "count": 67, "percent": 19.6 }, { "index": 3, "label": "Friend", "count": 32, "percent": 9.4 } ], "by_country": { "US": { "total": 180, "options": [80, 50, 30, 20] }, "GB": { "total": 45, "options": [20, 12, 8, 5] } }, "by_device": { "mobile": { "total": 210, "options": [90, 60, 40, 20] }, "desktop": { "total": 132, "options": [55, 38, 27, 12] } }, "by_referrer": { "twitter.com": { "total": 85, "options": [35, 25, 15, 10] }, "google.com": { "total": 60, "options": [20, 18, 12, 10] } }, "daily": [ { "date": "2026-03-01", "total": 28, "options": [12, 8, 5, 3] }, { "date": "2026-03-02", "total": 35, "options": [15, 10, 7, 3] } ]}Response fields:
| Field | Description |
|---|---|
options[].index | Position in the options array (0-based) |
options[].percent | Percentage of total responses, rounded to one decimal |
by_country | Keyed by ISO country code. options array matches the option order. |
by_device | Keyed by device type (mobile, desktop, tablet). |
by_referrer | Keyed by referrer domain. Only includes responses with a referrer. |
daily | One entry per day with responses. options array matches the option order. |
Errors:
404- Link not found, or no survey exists on this link
Styling
Customize the survey card colors to match your brand. All colors must be 6-digit hex values (e.g. #1a1a2e). Omitted properties use the default dark theme.
| Property | Default | Description |
|---|---|---|
bg_color | #0a0a0a | Page background and option button background |
text_color | #e5e5e5 | Question text and option text color |
button_color | #3b82f6 | Option border highlights and hover accent |
{ "style": { "bg_color": "#1a1a2e", "text_color": "#ffffff", "button_color": "#e94560" }}Only bg_color, text_color, and button_color are accepted. Unknown keys are silently ignored. Invalid hex values return a 400 error.
The card background is automatically derived from bg_color (slightly lighter) so you only need to set one background color.
How the redirect flow works
When a link has an active survey, the redirect adds one step between the click and the destination:
Click -> Expiry check -> Click limit -> Password -> Survey -> RedirectThe survey step:
- If the visitor already answered (cookie) and
show_onceis enabled, skip the survey - If the visitor is a social media bot, skip the survey
- Otherwise, show the survey page (no Javascript, loads instantly)
- Visitor clicks an answer or skip
- Response is recorded, cookie is set, visitor gets a 302 redirect
The click is tracked when the answer is submitted, not when the survey page loads. Survey page views don’t inflate click counts. The response is recorded in the background - the redirect fires immediately with zero added latency.
Javascript example
Create a link with a survey, wait for responses, then pull results:
const API = "https://nobsredir.com/api";const headers = { "X-API-Key": "nobs_your_key", "Content-Type": "application/json"};
// 1. Create a linkconst linkRes = await fetch(`${API}/workspaces/ws_abc123/links`, { method: "POST", headers, body: JSON.stringify({ target: "https://yoursite.com/pricing", domain: "go.yourco.com", slug: "pricing", }),});const link = await linkRes.json();
// 2. Attach a surveyawait fetch(`${API}/workspaces/ws_abc123/links/${link.id}/survey`, { method: "PUT", headers, body: JSON.stringify({ question: "Which plan interests you?", options: ["Starter", "Pro", "Enterprise"], show_once: true, }),});
// 3. Later - pull resultsconst results = await fetch( `${API}/workspaces/ws_abc123/links/${link.id}/survey/results?days=7`, { headers });const data = await results.json();console.log(`${data.total_responses} responses in 7 days`);for (const opt of data.options) { console.log(` ${opt.label}: ${opt.count} (${opt.percent}%)`);}Python example
import requests
API = "https://nobsredir.com/api"HEADERS = {"X-API-Key": "nobs_your_key"}WS = "ws_abc123"
# Create a linklink = requests.post(f"{API}/workspaces/{WS}/links", headers=HEADERS, json={ "target": "https://yoursite.com/blog", "domain": "go.yourco.com", "slug": "blog",}).json()
# Attach a surveyrequests.put( f"{API}/workspaces/{WS}/links/{link['id']}/survey", headers=HEADERS, json={ "question": "Was this article helpful?", "options": ["Yes", "No"], "show_once": True, },)
# Pull resultsresults = requests.get( f"{API}/workspaces/{WS}/links/{link['id']}/survey/results", headers=HEADERS, params={"days": 30},).json()
print(f"{results['total_responses']} responses")for opt in results["options"]: print(f" {opt['label']}: {opt['count']} ({opt['percent']}%)")MCP tools
If you’re using the MCP server, four survey tools are available:
| Tool | Description |
|---|---|
create_survey | Create or update a survey on a link |
get_survey | Get survey config and response count |
get_survey_results | Pull aggregated results with breakdowns |
delete_survey | Remove a survey and all responses |
These map directly to the API endpoints above.