ScaledApps OffTo.it

offto.it documentation

offto.it is a redirect service for things that can't be changed once they exist: printed QR codes, labels, signs and programmed NFC tags. You encode a short offto.it URL once; offto.it forwards each visitor to that code's destination, and the destination can be changed at any time without reprinting or reprogramming anything.

Every code is one of two kinds:

  • a short code such as offto.it/xK3p9, each with its own destination;
  • a namespace: a prefix such as offto.it/x/{token} that forwards a whole family of existing tokens to a URL pattern on your site, with no per-code setup.

Getting an account

Accounts, API tokens and namespaces are set up by ScaledApps staff today; there is no self-service sign-up. Email contact@scaledapps.com to get started or to change a code you can't change through the API.

Short URLs

https://offto.it/{code}              a short code
https://offto.it/{prefix}/{token}    a token under a namespace

Always encode the full https://offto.it/… address. http:// and www.offto.it are permanently redirected (301) to https://offto.it with the same path, which costs the scanner an extra round trip.

Short codes

  • Generated codes are 5 characters of base58: 1–9, A–Z and a–z without 0, O, I and l, so a code read aloud or typed from a worn label can't be confused. In the rare case the space is busy, a 6-character code is issued instead.
  • Custom codes (for example offto.it/spring-menu) are 1–64 characters of A–Z a–z 0–9 . _ -. A few words used by the service itself are reserved and can't be codes (such as admin, api, auth, docs, health).
  • Codes are case-sensitive: xK3p9 and Xk3P9 are different codes.
  • A code is never reissued, even after it is switched off or retired.

Namespaces

A namespace maps offto.it/{prefix}/{token} to a destination template on your site, for example https://example.com/item/{token}. It is meant for tokens you have already issued: they start resolving through offto.it with nothing to import.

  • The token must match a pattern agreed for that namespace (for example, a UUID). Anything else is a 404, so a namespace can't be used to send visitors to arbitrary pages.
  • The token is URL-encoded when it is put into the template.
  • A namespace's prefix never changes once created, because it is printed on every code under it.
  • Any single {prefix}/{token} can be given its own destination, which takes precedence over the namespace for that one token.
  • Each namespace has its own redirect status (302 unless agreed otherwise; see Redirects) and can be switched off as a whole.

Example: the permanent document links of saveto.it, offto.it/d/{id}, are a namespace that forwards to saveto.it.

Lookup order: a code with its own destination first, then the namespace. A path has at most two segments (one / after the host) and is at most 128 characters long.

Redirects and caching

A live code answers with an HTTP redirect to its destination:

HTTP/1.1 302 Found
Location: https://example.com/current-destination
Cache-Control: no-store, no-cache, must-revalidate, private
Pragma: no-cache
Referrer-Policy: no-referrer
StatusWhen to use it
302The default, and the right choice for almost every printed code: the destination is expected to change.
307Temporary, like 302, but the client must repeat the same method and body.
301, 308Permanent. Browsers and search engines may remember a permanent redirect and skip offto.it next time, so a later change of destination may not reach people who have already scanned. Use only when the destination will never move.
  • Never cached. Every redirect is sent with no-store, so each scan is a fresh lookup and a change of destination takes effect on the very next scan.
  • No referrer. Referrer-Policy: no-referrer means your destination does not see the offto.it URL that was scanned.
  • Query strings are not passed on. Visitors are sent to the destination exactly as stored; anything after ? on the scanned URL is ignored. Put any tracking parameters you need into the destination itself.
  • Scanning needs no cookies and sets none.

Inactive and unknown codes

These all answer 404 with a short page that says This link isn't active and suggests the visitor check with whoever provided the code:

  • a code that doesn't exist, or a namespace token that doesn't match the namespace's pattern;
  • any token under a namespace that has been switched off, unless that token has its own destination;
  • a code that has been switched off (active: false);
  • a code past its expiry (expires_at);
  • a code that has been retired by ScaledApps staff. A retired code stays reserved and is never reissued.

A switched-off, expired or retired code does not fall back to a namespace. If you gave one token under a namespace its own destination and then switch it off, that token stops working; it does not quietly go back to the namespace's destination. Switch it back on, or point it somewhere new, to restore it.

Links API

For apps that create codes as they print, or change destinations from their own systems. Base https://offto.it/api. Send Authorization: Bearer <token> and Accept: application/json; request bodies are JSON. Tokens are issued by ScaledApps staff, each with one or more scopes. Keep tokens on your server: never put one in a web page or an app people download.

Each token belongs to your account. It can read and change only your codes: the ones your tokens created, and any ScaledApps has assigned to your account. Any other code answers 404, exactly as if it didn't exist.

RequestScopeDoes
POST /api/linkslinks:createCreate a short code. 201 with the new code.
GET /api/links/{code}links:readLook a code up. 200.
PATCH /api/links/{code}links:updateChange a code's destination, switch it on or off, set its expiry. 200 with the updated code.
POST /api/links/batchlinks:createUp to 1,000 at once: {"links": [ … the same fields … ]}. 200 with a result per row (created, existing — found by source_app + source_ref — or invalid with why). A batch that would take the account past its plan makes nothing (403 plan_limit).
GET /api/links/{code}/qr.svg · .pnglinks:readThe code's QR, ready to print (SVG for print shops, PNG at 16 px a module).

The API works on short codes. Namespaces, and destinations for single tokens under a namespace, are managed by ScaledApps staff.

Create a code

curl -s https://offto.it/api/links \
  -H "Authorization: Bearer $OFFTO_TOKEN" \
  -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -d '{"target_url": "https://example.com/item/42", "source_app": "my-print-app", "source_ref": "item:42"}'
FieldMeaning
target_urlRequired. The destination: an http or https URL, up to 2,048 characters.
codeOptional custom code (see Short URLs). Leave it out to get a generated 5-character code, which makes the smallest QR code.
titleOptional label for your own reference, up to 255 characters.
source_app, source_refOptional, up to 64 and 128 characters: your app's name and your own id for the thing being labelled. When both are given the request is idempotent: repeating it returns the code your account already made for that pair (200, with "idempotent": true) instead of making another. The existing code is returned unchanged, even if target_url differs; use PATCH to change it. Safe to retry after a timeout.
expires_atOptional date and time (ISO 8601). After it, the code answers 404.
redirect_statusOptional: 302 (the default), 301, 307 or 308. See Redirects.

Response (the same shape is returned by every links endpoint):

{
  "code": "xK3p9",
  "short_url": "https://offto.it/xK3p9",
  "target_url": "https://example.com/item/42",
  "title": null,
  "source_app": "my-print-app",
  "source_ref": "item:42",
  "active": true,
  "expires_at": null,
  "hit_count": 0,
  "created_at": "2026-10-31T14:02:11+00:00"
}

Print short_url. hit_count is the number of scans so far (see Scan counts).

Change a destination

curl -s -X PATCH https://offto.it/api/links/xK3p9 \
  -H "Authorization: Bearer $OFFTO_TOKEN" \
  -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -d '{"target_url": "https://example.com/item/42/v2"}'

Send only the fields you want to change: target_url, title, active (true/false), expires_at (or null to remove the expiry) and redirect_status. The code itself can't be changed. The change applies to the next scan. A code can't be deleted; switch it off with {"active": false}. A code retired by ScaledApps stays off whatever you send; ask ScaledApps if you need it back.

Every change made through the API is recorded, with the name of the token that made it.

API errors

Errors are JSON with a message; validation errors also have an errors object keyed by field:

{
  "message": "That code is already taken.",
  "errors": { "code": ["Already in use."] }
}
StatusMeaning
401No bearer token, or the token is unknown or has been revoked.
403The token doesn't have the scope this request needs, or it isn't attached to an account (ask ScaledApps for a new one).
404No such code, or not one of yours (GET and PATCH).
409The custom code you asked for is already taken (including by a switched-off or retired code).
422A field is invalid: for example a missing or non-http(s) target_url, or a custom code with other characters or a reserved word.

AI tools (MCP)

AI assistants that speak the Model Context Protocol (Claude, Cursor, VS Code and others) can use offto.it directly. The address is https://offto.it/mcp, and it takes the same token as the Links API, in the same header. The token needs links:read to connect; what it can see and change is exactly the API's.

# Claude Code
claude mcp add --transport http offto https://offto.it/mcp --header "Authorization: Bearer <token>"

# Clients configured with JSON (the exact layout varies by client)
{"offto": {"url": "https://offto.it/mcp", "headers": {"Authorization": "Bearer <token>"}}}

Tools (each one appears only if the token has its scope):

  • create_link (links:create): create a code, with the same fields as POST /api/links. The result also says created: false when source_app + source_ref matched a code you already had.
  • get_link (links:read): look a code up, as GET /api/links/{code}. Read-only.
  • update_link (links:update): change a destination, switch a code on or off, or set its expiry, as PATCH /api/links/{code}. Marked as destructive, so assistants ask you before using it: the change reaches every printed copy at the next scan.

Errors come back as tool errors starting with a short code, for example code_taken: … or not_found: …. Changes are recorded like API changes, marked as made through MCP. Signing in without a token (needed for claude.ai connectors) is planned and not available yet.

Printing QR codes and NFC tags

  • Prefer generated short codes for anything new. https://offto.it/xK3p9 fits the small QR version 2 grid (25 × 25 modules). A long token such as a 36-character UUID under a namespace needs version 4 (33 × 33). On a 1-inch label that is the difference between roughly 1.0 mm and 0.76 mm per module, which matters on a label that will get dusty or be scanned in poor light.
  • Encode the full URL, starting https://, exactly as returned in short_url. Mind the case of the code.
  • Leave a quiet zone of at least 4 modules of blank space around the code.
  • Test a printed sample with a phone before printing in quantity, and again after the label is fixed in place.
  • NFC tags: write the same https:// URL as a URI record. Lock the tag after writing if it will be somewhere public; the destination can still be changed on offto.it.

ScaledApps can supply QR artwork for a code as SVG or PNG, with a 4-module quiet zone, on request.

Scan counts and privacy

Each scan of a short code adds one to its hit_count. For each scan it records, offto.it keeps the time, the code scanned and where it was sent, the browser's user agent and referrer, and a salted one-way hash of the IP address. Raw IP addresses are not stored. offto.it sends visitors on with no referrer, so your destination doesn't learn which code was scanned unless the destination URL says so.

  • Scans of tokens under a namespace are recorded the same way (this can be switched off for a namespace). They have no hit_count, because a namespace keeps no record per token.
  • Uptime checks from pingto.it are redirected like any other request but are not counted or recorded, so monitoring a code doesn't make it look in use.
  • Recording a scan never holds up the redirect: if it can't be recorded, the visitor is still sent on.

Limits

WhatLimit
Generated code5 characters of base58 (6 in the rare case of a busy space)
Custom code1–64 characters of A–Z a–z 0–9 . _ -; not a reserved word
Scanned pathAt most 2 segments and 128 characters
target_urlhttp or https, up to 2,048 characters
titleUp to 255 characters
source_app / source_refUp to 64 / 128 characters
Redirect status301, 302 (default), 307, 308

Questions, a new namespace, or anything not covered here: contact@scaledapps.com.