Skip to content

HTTP API ​

Masir exposes the same JSON routes used by its web interface.

There is no API-token system. Authenticate with a user session unless a route is marked public or uses the jobs bearer secret.

Session authentication ​

Sign in and save the cookie:

sh
curl -sS -X POST https://go.example.com/api/auth/login \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","password":"your-password"}' \
  -c cookie.txt

Send -b cookie.txt with later requests.

In multi-workspace mode, send workspace requests to that workspace's subdomain. The host selects the workspace. A request body cannot select one.

Request security ​

Unsafe browser requests with an Origin header must use an origin that matches the request host. This protects cookie-authenticated routes from cross-site form requests.

A command-line request without Origin is accepted.

Rate-limited requests return 429 and a Retry-After header. Resource lookups that the current user cannot access return 404 instead of revealing that a row exists.

Permission names ​

PermissionOwnerMemberViewer
workspace.manageYesNoNo
workspace.deleteYesNoNo
members.manageYesNoNo
links.manageYesYesNo
links.readYesYesYes
analytics.readYesYesYes

Health and host context ​

MethodRouteAuthenticationResult
GET/api/healthPublicApp and database health, or 503
GET/api/hostPublicHost and deployment context used by the interface

Authentication ​

MethodRouteInput or result
GET/api/auth/providersEnabled providers and registration state
POST/api/auth/registeremail, password
POST/api/auth/demoOptional turnstileToken; creates or resumes a demo
POST/api/auth/verifyVerification token
POST/api/auth/verify/resendSends a new verification link
POST/api/auth/loginemail, password; sets the session
POST/api/auth/logoutClears the session
POST/api/auth/forgotemail; always returns a neutral result
POST/api/auth/resettoken, password; invalidates all sessions
GET/api/auth/googleStarts or finishes Google OAuth
GET/api/auth/microsoftStarts or finishes Microsoft OAuth
GET/api/auth/identitiesConnected sign-in methods
DELETE/api/auth/identities/:idRemoves one identity; rejects the last one

The demo route returns 404 when the feature is off. It returns a workspace URL when it creates or resumes a live demo session.

Workspaces ​

MethodRouteRequired accessInput or result
GET/api/workspacesSigned inCurrent memberships
POST/api/workspacesVerified accountname, optional slug, optional linkPrefix
PATCH/api/workspacesworkspace.managename, linkPrefix, optional pathMode: 'preserve' | 'replace'
DELETE/api/workspacesworkspace.deleteSoft-deletes the current workspace
GET/api/workspaces/link-prefixesworkspace.manageRetained link paths
DELETE/api/workspaces/link-prefixes/:prefixworkspace.manageRevokes a retained link path
GET/api/workspaces/analyticsanalytics.readperiod, or from/to (YYYY-MM-DD), optional compare=previous
GET/api/workspaces/analytics.csvanalytics.readSame filters as /api/workspaces/analytics
GET/api/workspaces/slug-available?slug=Signed inWorkspace slug availability
POST/api/workspaces/logoworkspace.manageMultipart PNG, JPEG, GIF, or WebP
DELETE/api/workspaces/logoworkspace.manageRemoves the logo
POST/api/workspaces/transfer-ownershipmembers.manageuserId

Single-workspace mode rejects another workspace. Masir also rejects deletion of the only workspace on an instance.

Members and invitations ​

MethodRouteRequired accessInput or result
GET/api/workspaces/membersmembers.manageMemberships
GET/api/workspaces/members/optionslinks.manageActive members for assignment
PATCH/api/workspaces/members/:idmembers.manageOptional role, isActive
DELETE/api/workspaces/members/:idmembers.manageRemoves a non-owner
GET/api/workspaces/invitationsmembers.manageOpen invitations
POST/api/workspaces/invitationsmembers.manageemail, optional role
POST/api/workspaces/invitations/:id/resendmembers.manageReplaces the token and sends again
DELETE/api/workspaces/invitations/:idmembers.manageRevokes an invitation
POST/api/workspaces/invitations/acceptInvited usertoken

An invitation role is MEMBER or VIEWER. The default is MEMBER.

MethodRouteRequired accessInput or result
GET/api/linkslinks.readPaginated and filtered links
POST/api/linkslinks.manageCreates a link
POST/api/links/batchlinks.manageCreates up to 20 rows in one request
POST/api/links/bulklinks.manageTags, untags, assigns a campaign, or archives many links
POST/api/links/import/previewlinks.manageMultipart CSV preview (max 1 MB, 1000 rows)
POST/api/links/importlinks.manageCreates rows from a previewed import
GET/api/links/export.csvlinks.readCSV of the current list filters
GET/api/links/:idlinks.readLink and creator
PATCH/api/links/:idlinks.manageUpdates provided fields, including responsibility and archive
DELETE/api/links/:idlinks.manageSoft-deletes a link
GET/api/links/:id/analyticslinks.readperiod or from/to, traffic, optional compare=previous
GET/api/links/:id/analytics.csvlinks.readSame filters as /api/links/:id/analytics
GET/api/links/:id/historylinks.readLast 50 changes
POST/api/links/:id/aliaseslinks.manageslug
DELETE/api/links/:id/aliases/:sluglinks.manageRevokes the alias
GET/api/links/:id/qrlinks.readformat and size

List filters include page, perPage, sort, status, q, tags, exact normalized destination, campaignId, createdBy (me or a user id), archived (default false), and needsReview.

A bulk request sends { selection: { ids } | { filter }, action, tagId?, campaignId? }. action is tag, untag, assignCampaign, or archive. The server resolves a filter inside the workspace, caps the set at 500, and answers 422 above the cap. Ids outside the workspace answer 404 with no change. The response is { affected, results }. One audit event links_bulk_action { action, count } is written.

FieldTypeRule
destinationUrlstringRequired on create; HTTP or HTTPS
slugstringGenerated when empty
titlestring or nullOptional display title
startsAtnumber or nullUnix milliseconds
expiresAtnumber or nullMust follow startsAt
scheduledDestinationstring or nullFallback before opening
expirationDestinationstring or nullFallback after expiry
limitDestinationstring or nullFallback after the visit cap
maximumVisitsinteger or nullMinimum 1
passwordstring or nullSets, replaces, or clears the password
campaignIdstring or nullCannot combine with utmCampaign
utmSource, utmMedium, utmCampaign, utmTerm, utmContentstring or nullTracking values; utmMedium overrides the campaign medium
responsibleUserIdstring or nullActive workspace member
reviewAtnumber or nullUnix milliseconds
archivedbooleanSets or clears archived_at
tagsstring[]Up to 20 names
notesstring or nullPrivate workspace text
targetingobject or nullCountry and OS destinations

A slug change keeps the old slug as an alias unless keepOldSlug is false.

Batch create ​

POST /api/links/batch accepts { campaignId?, destinationUrl, title?, items } where items is up to 20 { clientKey, utmSource, utmMedium?, utmContent?, slug? }. It returns { results: [{ clientKey, status, link?, error? }] }. A validation error answers 422 with { rows: [{ clientKey, error }] } and creates nothing.

CSV import and export ​

POST /api/links/import/preview accepts multipart form field file. It returns { importId, fileHash, rowCount, rows } where each row is { row, values, errors }. Columns map by header name. Dates are ISO 8601 with an offset or Z. Tags use |.

POST /api/links/import accepts { importId, fileHash, rows }. It returns { results: [{ row, status, linkId?, error? }] } where status is created, already_imported, conflict, or error. A slug conflict never overwrites an existing link.

GET /api/links/export.csv uses the same filters as GET /api/links. Columns are the import columns plus short_url, created_at, and lifetime_clicks. notes is included only with links.manage. password_hash is never exported.

Public visitor routes ​

MethodRouteInput or result
GET/api/links/public/:slugUnlock-page link state
POST/api/links/verify-passwordslug, password; sets a grant cookie
POST/api/reportslug, reason; neutral acknowledgement

These routes do not need a user session.

Tags and campaigns ​

MethodRouteRequired access
GET/api/tagslinks.read
POST/api/tagslinks.manage
PATCH/api/tags/:idlinks.manage
DELETE/api/tags/:idlinks.manage
GET/api/campaignslinks.read
POST/api/campaignslinks.manage
GET/api/campaigns/:idlinks.read
PATCH/api/campaigns/:idlinks.manage
DELETE/api/campaigns/:idlinks.manage
GET/api/campaigns/:id/analyticslinks.read
GET/api/campaigns/:id/analytics.csvlinks.read

Campaign analytics accepts period or from/to, attribution, and optional compare=previous, matching the link analytics filters.

GET /api/campaigns/:id/analytics.csv, GET /api/links/:id/analytics.csv, and GET /api/workspaces/analytics.csv download the same range as CSV. The file starts with key,label,definition,value rows for metrics and report meta, then a blank line, then bucket,count series rows. When the JSON report has breakdowns, a section,label,count block follows (referrer, country, device, browser; campaign also includes source and medium). Workspace and campaign files add a slug,title,clicks top-links block. Notes and link ids are never included. The value-bearing header is intentional; range and traffic live in meta rows and in shared/analytics-metrics.ts rather than a # metric,... comment line.

A tag input is name. A campaign uses name, utmCampaign, and optional utmMedium.

Audit events ​

GET /api/admin/audit-events requires workspace.manage.

Filters include group, type, limit, and before. The response is newest first and includes nextBefore for cursor pagination.

Scheduled jobs ​

POST /api/jobs/alerts uses:

text
Authorization: Bearer <NUXT_JOBS_SECRET>

It runs every maintenance job that is due. It returns 404 when no secret is set and 401 for a wrong secret. On success it returns sent, the number of alerts sent, and demosDeleted, the number of demos deleted. jobs holds one report for each job, with job, status (ran, skipped, or failed), and an optional count, detail, or error. The click_event_partitions job runs every 24 hours. Its count is the number of months it checked, and its detail holds partitionsReadyThrough, the exclusive end date of the newest click_events partition. When a job fails, the other jobs still run and the route returns 500 after the other jobs ran.

Operator status ​

GET /api/admin/status uses either:

  • A session cookie for a user whose email is in NUXT_OPERATOR_EMAILS
  • Authorization: Bearer <NUXT_JOBS_SECRET>

It returns 404 when the user is not in NUXT_OPERATOR_EMAILS, when no secret is set, or for an invalid bearer token. On success, it returns:

json
{
  "version": "1.0.1",
  "jobs": [
    {
      "job": "expiry_alerts",
      "lastStartedAt": "2026-09-23T20:00:00.000Z",
      "lastSuccessAt": "2026-09-23T20:00:01.000Z",
      "lastErrorAt": null,
      "lastError": null,
      "nextDueAt": "2026-09-23T20:15:00.000Z",
      "overdue": false,
      "status": "ok",
      "nextAction": null
    }
  ],
  "signals": [
    {
      "key": "event_write",
      "state": "ok",
      "detail": null,
      "updatedAt": "2026-09-23T20:00:00.000Z"
    }
  ],
  "partitionsReadyThrough": "2026-12-01T00:00:00.000Z"
}

An overdue job (whose nextDueAt is older than two intervals) has overdue: true, status: "failed", and a recommended nextAction.

Upload delivery ​

GET /uploads/* serves file-storage objects. S3-compatible storage uses the configured public base URL instead.

Open source link management for teams. Released under the MIT License.