Skip to main content
On this page

Getting started

SendURL API

Create and manage short links, and read their clicks, from your own code. The API is REST, speaks JSON and authenticates with an API key from your console.

Base URL
https://api.sendurl.to/api/v1
Format
JSON in, JSON out, over HTTPS
Available on
Every plan, Free included
Get an API key

Getting started

Workspaces, projects and links

Everything you create belongs to a workspace. Every account has one, made at signup with a project called Default. A workspace holds projects, and every link belongs to exactly one project. The API calls these org (as in orgId) and project.

  • Your API key decides the workspace. URLs never name it, and nothing outside the key's workspace is visible: another workspace's link is a 404.
  • A link created without projectId goes into the Default project (or a project key's own project). PATCH /links/:id with projectId moves it.
  • A workspace can have up to 100 projects. Projects can be created through the API and renamed in the console; they can't be deleted.

Plan limits

The link limit belongs to the workspace and counts its links across all projects. Creating one more is a 403 link_limit_reached. GET /me and GET /links report the plan and usage.

Links per workspace by plan
PlanLinks per workspaceAPI access
Free100Included
Pro1,000Included

Authentication

Authenticate with an API key

Every request needs an API key, sent as a Bearer token in the Authorization header. Without a valid key the API answers 401.

  1. Create a keyOpen API keys in the console and choose Create key. Give it a name like “Production”.
  2. Choose its access and expiryWhole workspace or one project (below). Expiry: never, 30 days, 90 days or a year. Keys look like snu_ followed by 32 letters and digits; copy yours from the API keys page.
  3. Send it on every requestAs Authorization: Bearer snu_…, over https:// only. Store it in an environment variable such as SENDURL_API_KEY.

Keep keys secret. Call the API from your server, never from a browser or mobile app: anyone who can read the key can use it. If a key leaks, revoke it on the API keys page and create a new one.

Header

Authorization: Bearer $SENDURL_API_KEY
401 Unauthorized
{
  "error": "invalid_api_key",
  "message": "This API key was revoked"
}

Workspace keys and project keys

What each kind of key can do
EndpointWorkspace keyProject key
Links: create, list, read, update, deleteEvery project. May pass projectId.Its project only. Another projectId is 403 project_forbidden on writes, 404 project_not_found on GET /links.
Clicks and daily statsEvery link of the workspace.Links of its project. Others are 404.
GET / POST /projectsYes.No: 403 project_key_forbidden.
GET /meYes.Yes, with its project.
  • A key can't create, list or revoke keys. API keys, listing your workspaces (GET /orgs), renaming projects or the workspace, your profile and dashboard reports live in the console only; called with a key, those endpoints answer 403 session_required.
  • A revoked key fails from its next request, and an expired key from its expiry: 401 invalid_api_key, with message “Invalid API key”, “This API key was revoked” or “This API key has expired”.
  • A workspace can have 25 active keys at a time.
  • Links a key creates are recorded as created by the person who made the key (userId).

Clicks

List clicks

GET/api/v1/links/:id/clicks

Every recorded visit of the link, newest first, one page at a time. Use GET /links/:id for the total.

AccessAny API key, for links in its scope.

Path parameters

iduuidRequired
The link's id. Anything that isn't a link of the key's scope is 404 link_not_found.

Query parameters

limitintegerOptional
Page size, 1 to 10. Default 10.
cursorstringOptional
pagination.nextCursor from the previous page. Leave it out for the first page.

Response

200 OK. Click objects. There is no total here.

Errors

List clicks: errors
StatusCodeMeaning
404link_not_foundNo such link in the key's workspace (or, for a project key, its project).
400validation_errorlimit outside 1–10, a cursor this API didn't issue, or offset sent.

cURL

curl "https://api.sendurl.to/api/v1/links/5d8f2a1c-7b3e-4c9d-a0e1-6f2b8c4d1e7a/clicks?limit=1" \
  -H "Authorization: Bearer $SENDURL_API_KEY"
200 OK
{
  "clicks": [
    {
      "id": "a4c6e8f0-1b3d-4e5f-9a7b-2c4d6e8f0a1b",
      "linkId": "5d8f2a1c-7b3e-4c9d-a0e1-6f2b8c4d1e7a",
      "ip": "203.0.113.24",
      "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1",
      "referer": null,
      "country": "DE",
      "city": null,
      "device": "mobile",
      "deviceVendor": "Apple",
      "deviceModel": "iPhone",
      "browser": "Mobile Safari",
      "browserVersion": "18.6",
      "browserMajor": "18",
      "browserType": null,
      "engineName": "WebKit",
      "engineVersion": "605.1.15",
      "os": "iOS",
      "osVersion": "18.6",
      "cpuArchitecture": null,
      "source": "qr",
      "clickedAt": "2026-10-14T08:03:11.482Z"
    }
  ],
  "pagination": {
    "limit": 1,
    "nextCursor": "WyIyMDI2LTEwLTE0VDA4OjAzOjExLjQ4MjkxM1oiLCJhNGM2ZThmMC0xYjNkLTRlNWYtOWE3Yi0yYzRkNmU4ZjBhMWIiXQ",
    "hasMore": true
  }
}

Clicks

Retrieve a click

GET/api/v1/links/:id/clicks/:clickId

One click of the link, however old.

AccessAny API key, for links in its scope.

Path parameters

iduuidRequired
The link's id. Anything that isn't a link of the key's scope is 404 link_not_found.
clickIduuidRequired
The click's id, from the clicks list.

Response

200 OK. A click object.

Errors

Retrieve a click: errors
StatusCodeMeaning
404link_not_foundNo such link in the key's workspace (or, for a project key, its project).
404click_not_foundNo click with that id on this link.

cURL

curl "https://api.sendurl.to/api/v1/links/5d8f2a1c-7b3e-4c9d-a0e1-6f2b8c4d1e7a/clicks/a4c6e8f0-1b3d-4e5f-9a7b-2c4d6e8f0a1b" \
  -H "Authorization: Bearer $SENDURL_API_KEY"
200 OK
{
  "id": "a4c6e8f0-1b3d-4e5f-9a7b-2c4d6e8f0a1b",
  "linkId": "5d8f2a1c-7b3e-4c9d-a0e1-6f2b8c4d1e7a",
  "ip": "203.0.113.24",
  "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1",
  "referer": null,
  "country": "DE",
  "city": null,
  "device": "mobile",
  "deviceVendor": "Apple",
  "deviceModel": "iPhone",
  "browser": "Mobile Safari",
  "browserVersion": "18.6",
  "browserMajor": "18",
  "browserType": null,
  "engineName": "WebKit",
  "engineVersion": "605.1.15",
  "os": "iOS",
  "osVersion": "18.6",
  "cpuArchitecture": null,
  "source": "qr",
  "clickedAt": "2026-10-14T08:03:11.482Z"
}

Workspace

List projects

GET/api/v1/projects

The workspace's projects: Default first, then by name (ignoring case), each with its link count.

AccessWorkspace keys only. A project key gets 403 project_key_forbidden.

Query parameters

limitintegerOptional
Page size, 1 to 10. Default 10.
cursorstringOptional
pagination.nextCursor from the previous page. Leave it out for the first page.

Response

200 OK. Project objects and pagination with total.

Errors

List projects: errors
StatusCodeMeaning
400validation_errorlimit outside 1–10, a cursor this API didn't issue, or offset sent.
403project_key_forbiddenCalled with a project key.

cURL

curl "https://api.sendurl.to/api/v1/projects" \
  -H "Authorization: Bearer $SENDURL_API_KEY"
200 OK
{
  "projects": [
    {
      "id": "3f6a1d2c-8e4b-4f7a-9c0d-1b2e3a4f5c6d",
      "name": "Default",
      "isDefault": true,
      "linksCount": 9,
      "createdAt": "2026-09-02T14:21:07.315Z",
      "updatedAt": "2026-09-02T14:21:07.315Z"
    },
    {
      "id": "7c1e5a9b-2d4f-4e6a-8b3c-0f9d1e2a3b4c",
      "name": "Marketing",
      "isDefault": false,
      "linksCount": 3,
      "createdAt": "2026-10-01T10:00:00.000Z",
      "updatedAt": "2026-10-01T10:00:00.000Z"
    }
  ],
  "pagination": {
    "limit": 10,
    "nextCursor": null,
    "hasMore": false,
    "total": 2
  }
}

Workspace

Create a project

POST/api/v1/projects

Adds a project to the workspace, up to 100. Projects can't be deleted or renamed through the API; renaming is in the console.

AccessWorkspace keys only. A project key gets 403 project_key_forbidden.

Body parameters

namestringRequired
1–100 characters after trimming spaces. Unique in the workspace, ignoring case.

Response

201 Created. The new project.

Errors

Create a project: errors
StatusCodeMeaning
400validation_errorA field breaks a rule; issues says which.
403project_key_forbiddenCalled with a project key.
403project_limit_reachedThe workspace already has 100 projects.
409project_name_takenA project with that name (in any letter case) exists.

cURL

curl -X POST "https://api.sendurl.to/api/v1/projects" \
  -H "Authorization: Bearer $SENDURL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Marketing"
  }'
201 Created
{
  "id": "7c1e5a9b-2d4f-4e6a-8b3c-0f9d1e2a3b4c",
  "name": "Marketing",
  "isDefault": false,
  "linksCount": 0,
  "createdAt": "2026-10-01T10:00:00.000Z",
  "updatedAt": "2026-10-01T10:00:00.000Z"
}

Workspace

Check your key

GET/api/v1/me

Who the key is: its workspace, its project (null for a workspace key), the plan and how many links the workspace uses. A cheap way to test a key.

AccessAny API key.

Response

200 OK. role is always null for a key. keyPrefix is the key's first 12 characters, never the key.

Errors

Only the 401s every request can get.

cURL

curl "https://api.sendurl.to/api/v1/me" \
  -H "Authorization: Bearer $SENDURL_API_KEY"
200 OK
{
  "via": "key",
  "org": {
    "id": "9b2e4c1a-5f3d-4e8b-a1c7-2d6f8e0b3a95",
    "name": "Acme's workspace"
  },
  "role": null,
  "project": null,
  "plan": {
    "name": "Free",
    "linksLimit": 100,
    "visitsLimitPerLink": 100000
  },
  "usage": {
    "linksUsed": 12
  },
  "key": {
    "id": "b7d9f1a3-c5e7-4f9a-8b1c-3d5e7f9a1b2c",
    "name": "Production",
    "keyPrefix": "snu_XXXXXXXX",
    "expiresAt": null
  }
}

Reference

The click object

One visit of the short URL that was redirected. Device, browser, engine and OS fields are parsed from the user agent and are null when it doesn't say.

Click fields
FieldTypeDescription
iduuidThe click's id.
linkIduuidThe link that was visited.
clickedAtstringISO 8601, UTC.
sourcestring | null"qr" when the visit came through the QR code (?q), else null.
countrystring | nullTwo-letter ISO 3166-1 country code from Cloudflare's geolocation (T1 for Tor), null when unknown.
citynullNot recorded; always null.
refererstring | nullThe visitor's Referer header, if the browser sent one.
ipstring | nullThe visitor's IP address.
userAgentstring | nullThe visitor's User-Agent header.
devicestring | nullDevice type parsed from the user agent: desktop, mobile, tablet, …
deviceVendor, deviceModelstring | nulle.g. Apple, iPhone.
browser, browserVersion, browserMajor, browserTypestring | nulle.g. Chrome, 141.0.0.0, 141.
engineName, engineVersionstring | nullRendering engine, e.g. Blink.
os, osVersionstring | nulle.g. Android, 15.
cpuArchitecturestring | nullWhen the user agent says, e.g. amd64.

Reference

Pagination

Every list (GET /links, GET /links/:id/clicks, GET /projects) is cursor-paginated, at most 10 items per page. Pass pagination.nextCursor back as cursor until hasMore is false.

Query parameters

limitintegerOptional
1 to 10, default 10. Anything else (0, 11, 1.5, abc) is 400 validation_error.
cursorstringOptional
Opaque: use it as given. A cursor this API didn't issue is 400 validation_error.

The pagination object

limit
The page size used.
nextCursor
The next page's cursor, or null on the last page.
hasMore
Whether another page follows.
total
Items in scope, on links and projects. Clicks have no total; GET /links/:id has totalClicks.
  • Pages don't shift when links or clicks are added in between: the cursor marks a position, not a count.
  • offset is not supported and is refused with 400 validation_error. Other unknown parameters are ignored.

cURL · next page

curl "https://api.sendurl.to/api/v1/links?limit=2&cursor=WyIyMDI2LTEwLTEzVDA5OjEyOjQ0LjEyMzQ1NloiLCI1ZDhmMmExYy03YjNlLTRjOWQtYTBlMS02ZjJiOGM0ZDFlN2EiXQ" \
  -H "Authorization: Bearer $SENDURL_API_KEY"

Reference

Errors

Errors have a non-2xx status and one JSON shape: error, a stable code to branch on; message, a sentence for people that may be reworded; and, on validation_error only, issues, the list of what failed, each with a path naming the field. Some codes add fields of their own: link_limit_reached carries limit and current.

400 · Schema rules
{
  "error": "validation_error",
  "message": "Invalid request",
  "issues": [
    {
      "validation": "url",
      "code": "invalid_string",
      "message": "Invalid url",
      "path": [
        "destinationUrl"
      ]
    },
    {
      "validation": "regex",
      "code": "invalid_string",
      "message": "Slug must be 1-256 characters: letters, digits, '-' or '_'",
      "path": [
        "slug"
      ]
    }
  ]
}
400 · Expiry and password rules
{
  "error": "validation_error",
  "message": "Expiry must be in the future",
  "issues": [
    {
      "code": "custom",
      "path": [
        "expiresAt"
      ],
      "message": "Expiry must be in the future"
    }
  ]
}
403 · Link limit
{
  "error": "link_limit_reached",
  "message": "You have reached the maximum of 100 links on the Free plan.",
  "limit": 100,
  "current": 100
}
Error codes
StatusCodeMeaning
400validation_errorA body field, query parameter or cursor breaks a rule. issues lists them.
400invalid_jsonThe body isn't valid JSON.
400bad_requestThe request couldn't be read, e.g. a body cut short. The status is the one that fits: 400, or 415 for an unsupported charset or Content-Encoding.
401missing_tokenNo Authorization: Bearer … header.
401invalid_tokenThe Bearer token is neither an API key (snu_…) nor a valid console session.
401invalid_api_keyAn snu_ key that isn't valid. message: “Invalid API key” (malformed or unknown), “This API key was revoked” or “This API key has expired”.
403session_requiredA console-only endpoint: API key management, listing workspaces (GET /orgs), renaming projects or the workspace, profile, reports.
403project_forbiddenA project key named another project in a write.
403project_key_forbiddenA project key called /projects.
403link_limit_reachedThe workspace has used all its plan's links.
403project_limit_reachedThe workspace has 100 projects.
403link_moderatedRe-enabling or deleting a link disabled by moderation.
404link_not_foundNo such link in the key's scope.
404click_not_foundNo such click on that link.
404project_not_foundNo such project in the workspace (or the key's scope).
404not_foundNo such endpoint.
409slug_takenThe custom slug is in use, or (rarely) a random slug collided: retry.
409project_name_takenA project with that name exists.
413payload_too_largeThe body is too large.
500internal_errorSomething failed on our side.
503service_unavailableWe couldn't check the key just now. Retry.

Reference

Rate limits

There is no per-key limit. Cloudflare, in front of the API, allows 50 requests per 10 seconds from one IP address; past that it answers 429 for 10 seconds. That response comes from Cloudflare, not the API, so it isn't in the JSON error shape. Wait and retry with backoff.