CONNECTED QR REDIRECTS
Know when a visitor arrives—and react in real time.
ScanSocket turns a short URL into a connection point. A visitor follows the URL and reaches your destination immediately; your application receives an authenticated event and can send JSON data back to that specific visit.
Provide an HTTP or HTTPS destination and receive a short URL plus QR code.
Each visit creates a private connection ID before returning a 307 redirect.
Your backend consumes the event and can address that visitor's receive-only stream.
Use it when a physical scan or shared link should trigger backend work: check-in flows, device pairing, interactive exhibits, pickup screens, or cross-device handoffs. It redirects traffic; it does not host your destination page.
CORE CONCEPTS
The four objects in an integration.
Redirect
A persistent short URL associated with one destination URL. It can optionally expire and can be deleted by its owner.
Connection
One visit to a short URL. ScanSocket assigns it an opaque connection ID and records a connected event.
Owner stream
An authenticated stream of visits for one redirect. Your backend consumes it with the client or HTTP API.
Visitor channel
Public receive and send endpoints addressed by connection ID. Your destination can exchange JSON messages with the owner.
Account and guest workspaces
You can try one redirect as a guest in the web workspace. Guest ownership is tied to a signed, HttpOnly browser cookie and does not provide API or client-library access. Sign in to manage multiple redirects and create revocable API tokens for integrations.
QUICK START
Create a connected redirect from your backend.
- Create an API token
Open the workspace, sign in, and create a token under Developer access. Copy it immediately; the raw token is shown only once.
- Store the token on your server
SCANSOCKET_API_TOKEN=ss_live_...Never include this value in browser code or a public repository.
- Install the client
npm install @scansocket/client - Create a redirect
import { ScanSocketClient } from "@scansocket/client"; const client = new ScanSocketClient( "https://scansocket-9eq5sywt5-sandro-padins-projects.vercel.app", process.env.SCANSOCKET_API_TOKEN!, ); const redirect = await client.createRedirect({ url: "https://your-app.example/welcome", // expiresAt: "2027-01-01T00:00:00.000Z", // visitorMessaging: "authenticated" | "public", // Default: "disabled" }); console.log(redirect.shortUrl); // Put this URL in your own QR code or link console.log(redirect.qrCode); // Or render this PNG data URL directly - Listen and react
const unsubscribe = client.subscribe( redirect, (event) => { switch (event.type) { case "connected": console.log("Visitor connected", event.data.connectionId); void client.sendMessage(redirect, event.data.connectionId, { status: "ready" }); break; case "message": console.log("Visitor message", event.data.connectionId, event.data.data); break; case "disconnected": console.log("Visitor disconnected", event.data.connectionId); } }, (error) => console.error("Event stream stopped", error), ); // Call this when your process, request, or component ends. unsubscribe();
DESTINATION HANDOFF
Read the connection ID from the destination URL.
Each successful visit adds scanSocketConnectionId to the destination URL. The owner receives the same value as connectionId in the connected event.
Existing query parameters and the URL fragment are preserved.
Redirect semantics
The 307 status preserves the request method, although public short URLs currently handle GET. Missing redirects return 404. Expired redirects return 410 and do not forward the visitor.
CLIENT REFERENCE
A small API with explicit stream lifecycle control.
new ScanSocketClient(baseUrl, token, fetcher?)Creates a client for a ScanSocket deployment. baseUrl is the deployment origin, with or without a trailing slash. token is required. Supply a compatible fetch implementation only for testing or a specialized runtime.
client.createRedirect({ url, expiresAt?, visitorMessaging? })Creates an account-owned redirect. Visitor messaging defaults to disabled; explicitly choose authenticated or public to enable it. url must be absolute HTTP(S), and expiresAt must be a future ISO 8601 date-time.
client.events(redirect, { signal?, cursor? })Returns an async iterator of owner events. It reconnects after a normal server close and continues after the last event ID. Use cursor to resume after a previously persisted numeric event ID.
client.subscribe(redirect, onEvent, onError?)Consumes events() in the background and invokes onEvent in order. Returns a function that aborts the stream. onError receives terminal request or parsing failures.
client.sendMessage(redirect, connectionId, data)Sends any JSON-serializable value to a connection that belongs to the redirect. Resolves after the server accepts the message; throws on an unsuccessful response.
new ScanSocketVisitorClient(baseUrl).sendMessage(connectionId, data)Sends from browser code without an owner token. This works only when the redirect explicitly uses visitorMessaging: "public".
client.sendVisitorMessage(connectionId, data)Forwards a visitor message from trusted backend code with the owner token. Use this with visitorMessaging: "authenticated".
Redirect result
idStable redirect identifier used by API paths.shortUrlPublic URL to share or encode.qrCode512 px PNG QR code as a base64 data URL.eventsUrlAuthenticated owner-event endpoint used by the client.expiresAtISO timestamp, or null when the redirect does not expire.visitorMessagingdisabled, authenticated, or public.Async iteration and backpressure
const controller = new AbortController();
for await (const event of client.events(redirect, {
signal: controller.signal,
cursor: 0,
})) {
await processEvent(event); // Events are yielded in order
}
controller.abort();HTTP API
Use bearer authentication from trusted code.
Send Authorization: Bearer $SCANSOCKET_API_TOKEN to every account API. JSON responses use Content-Type: application/json.
POST /api/redirectsCreates a redirect and returns it with status 201. The JSON body accepts url, optional expiresAt, and optional visitorMessaging. Messaging defaults to disabled.
curl https://scansocket-9eq5sywt5-sandro-padins-projects.vercel.app/api/redirects \
--request POST \
--header "Authorization: Bearer $SCANSOCKET_API_TOKEN" \
--header "Content-Type: application/json" \
--data '{"url":"https://your-app.example/welcome"}'GET /api/redirectsReturns { "redirects": [...] } for the token's account, newest first.
curl https://scansocket-9eq5sywt5-sandro-padins-projects.vercel.app/api/redirects \
--header "Authorization: Bearer $SCANSOCKET_API_TOKEN"DELETE /api/redirects/:idDeletes an owned redirect and returns 204. Future short-URL visits then return 404. Returns 404 when the redirect is missing or belongs to another account.
GET /api/redirects/:id/eventsOpens the authenticated owner stream. It defaults to Server-Sent Events; request JSON Lines with ?format=jsonl or Accept: application/x-ndjson.
POST /api/redirects/:id/connections/:connectionId/messagesSends { "data": value } to one connection and returns 204. The value must be JSON-serializable and no larger than 16 KB after serialization.
POST /api/connections/:connectionId/messagesAppends an ordered visitor message event. Public redirects accept the connection capability alone; authenticated redirects require the owning bearer token; disabled redirects reject all requests.
Common errors
400Invalid URL, expiration, request body, or message.401Missing, invalid, or revoked bearer token.403Visitor messaging is disabled for the redirect.404Redirect or connection not found in the authenticated account.413Visitor-message request body exceeds 20 KB.429More than 30 visitor messages were sent for one connection in 10 seconds. Honor Retry-After.EVENT STREAMS
Resume reliably from the last processed event.
The owner stream emits events in ascending numeric ID order. Each current visit produces a connected event:
{
"id": 42,
"redirectId": "d0b18e0f-...",
"type": "connected",
"occurredAt": "2026-07-11T18:24:03.000Z",
"data": {
"connectionId": "93a70b4b-...",
"userAgent": "Mozilla/5.0 ...",
"referer": "https://example.com/"
}
}Visitor metadata is best-effort. ip, userAgent, and referer may be absent; do not make core behavior depend on them. Treat connectionId as an opaque string.
Server-Sent Events
The default response uses text/event-stream. Resume with the Last-Event-ID header or ?cursor=<id>. Event names match the event type; each data field contains the complete JSON event.
JSON Lines
The client requests application/x-ndjson. Each non-empty line is one complete event. Pass ?cursor=<id> to receive events after that ID.
The server closes a stream before the platform execution limit. The client reconnects automatically after a normal close. If you use HTTP directly, reconnect and send the last fully processed event ID; duplicate-safe processing is recommended.
Owner event types
connected carries { connectionId, ip?, userAgent?, referer? } for the initial visit and { connectionId } after a disconnected visitor returns. message carries { connectionId, data }. disconnected carries { connectionId }. All share id, redirectId, and occurredAt.
VISITOR MESSAGES
Send data to one specific visit.
Visitor-to-owner messaging is off by default and is configured when the redirect is created. This setting does not affect owner-to-visitor messages or visitor SSE presence.
disabledDefault. All visitor-to-owner posts return 403, even with an owner token.authenticatedYour application server validates the visitor and forwards the message with its ScanSocket owner token.publicBrowser code may post without an owner token. The connection ID authorizes the operation.Public browser messaging
import { ScanSocketVisitorClient } from "@scansocket/client";
const connectionId = new URL(window.location.href).searchParams.get(
"scanSocketConnectionId",
);
if (!connectionId) throw new Error("Missing ScanSocket connection ID");
const source = new EventSource(
`https://scansocket-9eq5sywt5-sandro-padins-projects.vercel.app/api/connections/${connectionId}/events`,
);
source.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
console.log(message.data);
});
// Sending without a token requires visitorMessaging: "public".
const visitor = new ScanSocketVisitorClient("https://scansocket-9eq5sywt5-sandro-padins-projects.vercel.app");
await visitor.sendMessage(connectionId, { answer: "B" });
// Later, close the stream when the destination no longer needs it:
// source.close();Authenticated server forwarding
// Trusted application backend only. First authenticate and validate
// the visitor request using your application's normal session.
await client.sendVisitorMessage(connectionId, { answer: "B" });Never embed or forward the ScanSocket owner token to the visitor. Authenticate and validate the visitor request in your own backend before calling sendVisitorMessage.
Each SSE message contains id, connectionId, sentAt, and your original data value. Resume with Last-Event-ID or ?cursor=<id>.
Presence and reconnects
The initial short-link visit creates the first connected event. Opening the visitor SSE stream marks that connection active. When every stream for the connection disappears, ScanSocket waits 10 seconds before appending disconnected. Reconnecting within the grace period preserves the session; returning later appends another connected.
Duplicate tabs and multiple EventSource instances create independent leases, so closing one does not disconnect the connection while another remains. Normal closes, network loss, and the server's planned stream rotation use the same grace behavior. Expiration prevents new short-link visits but does not terminate an existing connection. Deleting a redirect cascades its connections and no final disconnected event is retained.
Anyone who has a connection ID can read owner messages and, in public mode, send visitor messages for that connection. Do not log, publish, or reuse it as a user identity or authentication credential. Never send secrets through this channel unless your application adds suitable end-to-end protection.
SECURITY & LIMITS
Design the integration around clear trust boundaries.
- Keep API tokens in server-side environment variables or a secrets manager. The client supports browsers technically, but privileged methods should run only in trusted code.
- Visitor messaging defaults to
disabled. Preferauthenticatedfor messages affecting identity, scores, purchases, permissions, or other trusted state. Usepubliconly for deliberately anonymous, low-risk input. - Create separate tokens for development, staging, and production so you can revoke each independently.
- A token can create, list, and delete redirects and consume events for its account. Raw token values are not stored and are shown only once.
- Revoking a token immediately blocks authenticated API and owner-stream access through that token. It does not disable existing public short URLs.
- Delete a redirect to stop future visits. Use
expiresAtwhen it should stop automatically at a known time. - Owner and visitor messages are limited to 16 KB of UTF-8 serialized JSON. Visitor requests are limited to 20 KB and 30 messages per connection per 10-second window.
- Connection and event streams may reconnect. Resume from the last processed cursor and make handlers duplicate-safe.