← Back to s.plings.io Core Systems

URL Structure & Parameters

Created: Tue 29 Jul 2025 06:49:39 CEST
Updated: Wed 06 May 2026 17:14:18 CEST - Phase 1 naming migration: clarified transport-agnostic URL format (QR / NFC / RFID); resolved native-app integration architecture (apps call GraphQL API directly; Gateway handles browser path)
Updated: Sun 14 Jun 2026 10:17:12 CEST - Scan landing Phase 1: routing convergence (all known objects → plings.io/o/{oid}); new sid redirect parameter; new unverified error code; UNKNOWN now routes to /welcome
Updated: Sun 14 Jun 2026 11:24:14 CEST - Path format corrected to base58-encoded segments (was wrongly documented as decimal integers 1–999999); matches the Gateway validator and the minting tool.
Updated: Mon 15 Jun 2026 - Error handling aligned to implemented firewall behavior: rate-limited requests return HTTP 429 (not a redirect) and internal errors return HTTP 500; added the fixed details token table (incl. bad_transport for t not in q/n/r/b).
Updated: Sun 21 Jun 2026 - added 1D Code128 identifier: payload format, transport t=c, resolution model, and Gateway bypass note.
Updated: Fri 26 Jun 2026 11:12:05 CEST - Documented variable path depth (5–10 levels): new “Path Depth & Sub-Batches” section explaining positional role assignment (last two segments are always batch.instance, middle is a 0…N category list), per-level value ranges, the role-shift caveat when adding depth, and flat-numbering vs. nested-sub-batch issuance. Clarified the Path Structure Breakdown as the 5-level minimum, not a fixed structure.
Updated: Fri 26 Jun 2026 14:40:00 CEST - Per-level value cap reconciled to the BIP32 ceiling 2³¹−1 (uniform across levels); removed the stale small decimal-cap table (legacy spärr with no rationale). Gateway now rejects segments above the ceiling. ADR-0051.
Document Version: 1.8 - per-level value cap = BIP32 ceiling (2³¹−1); legacy decimal caps removed
Security Classification: Public Technical Documentation
Target Audience: Frontend Developers, Backend Developers
Author: Paul Wisén

Overview

This document details the URL structure and parameter specifications for the s.plings.io Gateway service. The same URL format is used for every scan transport — QR codes, NFC tags, RFID tags, and (future) Bluetooth beacons all encode this identical URL.

Incoming Scan URL Format

Base Structure

https://s.plings.io?t=<transport>&i=<instance>&p=<path>&cp=<class_pointer>

Core Parameters

Parameter Name Required Description Valid Values Example
t Transport Yes Scan transport q, n, r, b q (QR code)
i Instance Key Yes Unique instance identifier Base58 string 4kyQCd5tMDjJVWJH5h95gUcjq3qTX2cj5nwjVyqBRwLo
p Path Yes HD wallet derivation path Dot-separated base58 segments 4.2.3.3.6
cp Class Pointer No Cryptographic class identifier Base58 string 4K7mX9abDcE

Extended Parameters (Future)

Parameter Name Description Example
tx Transaction Transaction type buy, rent, service
svc Service Service identifier repair, insurance
price Price Amount in cents 2500
dur Duration Time period 1d, 1w, 1m

Path Structure Breakdown

The path parameter (p) follows the HD wallet hierarchy. The minimum, canonical form is 5 levels:

p=4.2.3.3.6
  │ │ │ │ └── Instance ID
  │ │ │ └──── Batch/Series
  │ │ └────── Category
  │ └──────── Plant/Location
  └────────── Manufacturer ID

The structure is variable-depth, not fixed at 5. See Path Depth & Sub-Batches below for how levels are interpreted when a path is deeper, and how to organise issuance into nested sub-batches.

Path Depth & Sub-Batches

A Plings path is variable-depth: it may have between 5 and 10 levels (the 10-level ceiling is enforced by the Gateway validator — see Path validation). The depth is not fixed because the middle of the path is an arbitrary-length list of category levels.

How depth is interpreted

The parser (Plings-API/scripts/utils/path_utils.py) assigns roles positionally, anchored from both ends:

4 . 1 . 2 . 7 . 3 . 1
│   │   └───┬───┘   │ └── instance   (always the LAST segment)
│   │       │       └──── batch      (always the SECOND-TO-LAST segment)
│   │       └──────────── categories (ZERO OR MORE middle segments)
│   └──────────────────── manufacturer
└──────────────────────── wallet version

⚠️ Roles shift when you add depth. Because batch and instance are always the last two segments, adding a level re-labels the segment that used to be the batch into a category. Going from 3.1.2.1.5 (instance 5 of batch 1) to 3.1.2.1.5.5 does not add a sub-level under instance 5 — it makes [2, 1] the categories, 5 the batch, and the new trailing 5 the instance. Plan the full depth up front for a given issuance line rather than appending levels later.

Per-level value range

Every level shares the same bounds: 1 … 2³¹−1 (2 147 483 647). The position determines a level’s semantic role (wallet / manufacturer / category / batch / instance), not its allowed magnitude. The ceiling is the BIP32 hardened-index limit — each level becomes a hardened index value + 0x80000000 that must fit in 32 bits.

Earlier revisions of this doc listed small per-level decimal caps (wallet ≤ 99, manufacturer ≤ 999, category ≤ 9999, batch/instance ≤ 99999). Those were an undesigned legacy spärr and have been removed — they had no rationale and contradicted the Gateway. See [ADR-0051]. The only structural limit besides this value ceiling is the 10-level depth cap.

Flat numbering vs. nested sub-batches

Within a path you “own” (e.g. 3.1.2.1 = wallet 3 · manufacturer 1 · category 2 · batch 1), there are two valid ways to issue identifiers:

Both are fully supported. Nested sub-batches are useful for separating issuance lines (per product type, per print run, per delegated range) while keeping a shared prefix. The only hard constraint is the 10-level total — deep nesting plus the wallet/manufacturer/batch/instance anchors still has to fit inside 10 segments — and the per-level ranges above.

1D Code128 Payload (<prefix>-<path>)

Code128 is a linear (1D) barcode used for long round objects — fishing rods, cable sections — where a 2D QR code does not fit due to the object’s curvature. Only a narrow flat strip along the long axis faces the camera; a 1D barcode oriented along that axis fits this geometry perfectly.

Payload format

<prefix>-<path>

Example payload at prefix length 12:

CCuejfEAmFGK-4.2.3.2.2

Not a URL

The 1D payload is deliberately not a URL. The stock iOS Camera does not reliably follow a link from a 1D scan (that behaviour is effectively a QR feature), so Code128 scanning is app-scan territory regardless. Native apps decode the barcode, parse the <prefix>-<path> token, and call the GraphQL API directly — 1D scans bypass the Gateway entirely.

Transport tag t=c

The transport value for Code128 is t=c. As with all transports, it is analytics only — routing never branches on transport. Because the 1D payload is not a URL, the barcode itself carries no t field; the scanner sets t=c itself when it decodes a Code128 symbol and passes it to the GraphQL API.

Normalization to the unified resolution model

After parsing, both a QR URL and a 1D Code128 payload normalize to the same canonical tuple:

URL  "…?t=q&i=<full>&p=<path>"  ─┐
                                  ├─►  { path, keyMaterial }  ──►  one resolver + verifier
1D   "<prefix>-<path>"          ─┘

keyMaterial is the full instance key (from QR) or the scanned prefix (from 1D) — the same axis at different lengths. Resolution and verification proceed identically:

  1. Resolve the ObjectInstance by path (the value common to every identifier form).
  2. Verify by comparing the scanned keyMaterial (length L) against the stored instance key truncated to L. A match confirms the identifier to strength L.

The backend enforces a minimum prefix length (≥ 8 characters) and rejects shorter prefixes.


Outgoing Redirect Parameters

After processing, s.plings.io enriches the URL with additional parameters:

Standard Redirect Parameters

Parameter Name When Included Description Example
oid Object ID When object exists Database UUID for object obj-550e8400-e29b-41d4
ikey Instance Key Always Original scanned identifier 4kyQCd5tMDjJVWJH5h95gUcjq3qTX2cj5nwjVyqBRwLo
path HD Path Always Base58-encoded wallet path 4.2.3.3.6
class Class ID When resolved Class UUID 550e8400-e29b-41d4-a716-446655440000
cptr Class Pointer When available Cryptographic class pointer forwarded from incoming cp 4K7mX9abDcE
sid Scan Event ID When scan event is logged Neo4j scan event node ID; used by the browser landing page to enrich the event with coarse geolocation in Phase 2 scan_789xyz
src Source Always Tracking source scan
loc Location With consent GPS coordinates 59.3293,18.0686
ts Timestamp Optional Scan timestamp 1699123456789

URL Transformation Examples

Example 1: Known Object for Sale

Input:
https://s.plings.io?t=q&i=4kyQCd5tMDjJVWJH5h95gUcjq3qTX2cj5nwjVyqBRwLo&p=4.2.3.3.6

Processing:
- Identifier verified ✓
- Object found in database
- Status: FOR_SALE
- Scan event logged (sid=scan_789xyz)
- Route to /o/:oid (status-aware; page renders "For Sale" notice)

Output:
https://plings.io/o/obj-123?oid=obj-123&ikey=4kyQCd5tMDjJVWJH5h95gUcjq3qTX2cj5nwjVyqBRwLo&path=4.2.3.3.6&sid=scan_789xyz&src=scan

Example 2: Unknown Generic Tag

Input:
https://s.plings.io?t=q&i=7mnPQd8uNEkKWXKI6i06hVdjr4rUY3dk6oxkWyrCSwMp&p=1.1.1.1.1

Processing:
- Identifier format valid ✓
- No object found in database
- Generic Plings tag (no class pointer)
- Scan event logged (sid=scan_456def)
- Route to unknown-tag onboarding

Output:
https://plings.io/welcome?ikey=7mnPQd8uNEkKWXKI6i06hVdjr4rUY3dk6oxkWyrCSwMp&path=1.1.1.1.1&sid=scan_456def&src=scan

Example 3: Manufacturer Pre-printed Tag

Input:
https://s.plings.io?t=q&i=9abCDe3vOPqRStUvWxYz&p=17.3.42.1.1001&cp=3K7mX9

Processing:
- No object instance found
- Class pointer present (forwarded as cptr=3K7mX9)
- Scan event logged (sid=scan_abc123)
- Route to unknown-tag onboarding (class-pointer-enriched; /welcome shows class info if class system resolves cptr — stubbed for now)

Output:
https://plings.io/welcome?ikey=9abCDe3vOPqRStUvWxYz&path=17.3.42.1.1001&cptr=3K7mX9&sid=scan_abc123&src=scan

Service-Specific Routing

v1 routing (current): all known objects (any status with a resolved object_id) route to the single status-aware landing page at plings.io/o/{oid}. The page branches on the object’s status itself. Unknown/unregistered tags route to the onboarding page.

Object Status Destination (v1) Notes
NORMAL plings.io/o/{oid} Standard object page
FOR_SALE plings.io/o/{oid} Page shows “for sale” notice; transaction handled in app
FOR_RENT plings.io/o/{oid} Page shows “for rent” notice; transaction handled in app
LENDABLE plings.io/o/{oid} Page shows “lendable” notice; transaction handled in app
LOST plings.io/o/{oid} Page shows “lost” alert with prominent contact-owner CTA
UNKNOWN (no object_id) plings.io/welcome Unknown/unregistered tag onboarding page

Future work: Dedicated subdomains (market.plings.io, rent.plings.io, lend.plings.io) are planned for when commerce features are built. They will receive the same oid and scan parameters. Switching to them does not require reprinting physical identifiers — only a Gateway routing rule change.

Parameter Validation Rules

Instance Key (i)

Path (p)

Transport (t)

Class Pointer (cp)

Error Handling

Invalid or unverifiable parameters result in a 302 redirect to the single /scan-error page. Plings-Web exposes one /scan-error React route, so the destination is always a query-param route — a sub-path like /scan-error/invalid would render NotFound. The details value is always one of a fixed set of tokens — raw user input is never echoed:

https://plings.io/scan-error?reason=invalid&details=<token>
https://plings.io/scan-error?reason=unverified
details token Meaning
missing_params Required parameter i or p absent
bad_transport t is not one of q, n, r, b, c
bad_instance_key i is not Base58 or not 32–48 characters
bad_path p violates the path rules above
bad_class_pointer cp is not Base58

Rate-limited requests do not redirect (forwarding a flood to plings.io would defeat the firewall). They receive an HTTP 429 JSON response with Retry-After and X-RateLimit-* headers. Internal errors return an HTTP 500 JSON response with a sanitized message — internal error details never leave the Gateway.

Error reason codes

reason Meaning User-facing message intent
invalid The code is structurally malformed (bad Base58, missing required parameters) “This code is not a valid Plings identifier”
unverified The code is well-formed but could not be verified as genuine (cryptographic verification failed or is pending) “We could not verify this tag is genuine”
system Internal server error (also returned as an HTTP 500 JSON response rather than a redirect) “Something went wrong on our end”

Note on unverified: Cryptographic genuineness verification (HD wallet signature check) is stubbed as of Phase 1. The unverified error path is wired and the /scan-error page renders appropriate genuineness messaging, but the actual verification logic is not yet implemented. This will be completed in a future phase.

Security Considerations

Native App Integration

Native apps (Plings-iOS, Plings-Android — planned) bypass the Gateway and call the GraphQL API directly. This is intentional: native NFC interactions need to be instant, and a network roundtrip to s.plings.io purely for redirection would defeat the point.

The unified contract is the GraphQL API at api.plings.io, not the Gateway’s HTTP front door. The Gateway exists for entry points that don’t have a native app to do their own resolution (browser, e-mail link, OS camera).

Native scan flow

  1. User taps an NFC tag (or scans a QR) inside the native app
  2. The app parses the URL parameters from the scan payload — same parameters documented above
  3. The app calls a GraphQL query (e.g., resolveIdentifier) with the parsed i/p/cp/t values
  4. The app routes the user locally based on the response (object data, status, action hints)
  5. The app reports a scan event back to the API for centralized analytics
// Native scan path — call GraphQL directly
const { data } = await api.resolveIdentifier({
  instanceKey: params.i,
  path: params.p,
  classPointer: params.cp,
  transport: params.t,
});
// Then act on data.object.status / data.routing locally

When the Gateway still handles a scan from a phone with the app installed

When a s.plings.io URL is opened from outside the app (a friend shares the link, the user scans with the OS camera and chooses the browser, etc.), the OS hands the URL to whichever app is registered. If the Plings app is registered as a universal/app link handler, it can intercept and run the native flow. Otherwise the browser hits s.plings.io and the Gateway routes normally.

Where centralized concerns live

Logging, rate limiting, validation, and authorization are not the Gateway’s exclusive responsibility — they live behind the GraphQL API and apply to every caller, native or web. The Gateway adds an extra firewall layer specifically for browser-side abuse (DDoS, automated scrapers); native apps can’t reach the API without their own credentials and rate limits.