Resolution API
These endpoints are public: no API key is required. They run in two places:
- Your link domain (the host your short URLs use): the redirect page, the
/dataJSON twin, and the/.well-known/*association files. The host header selects the organization's configuration. - Your API host:
POST /match, the endpoint the mobile SDKs call on first open.
How a click turns into an app open is explained in Routing & fallbacks and Deferred deep linking & the match flow; this page is the wire contract.
| Method | Path | Purpose |
|---|---|---|
GET | /:orgKey/:shortCode | Redirect page (HTML) for browsers |
GET | /:orgKey/:shortCode/data | Link data as JSON, for app-direct opens |
POST | /match | Deferred deep-link match (SDK first open) |
GET | /.well-known/apple-app-site-association | iOS Universal Links configuration |
GET | /.well-known/assetlinks.json | Android App Links configuration |
orgKey is exactly 4 alphanumeric characters; anything else returns 400 before any link lookup.
Resolve the redirect page
GET /:orgKey/:shortCode
Statuses:
| Status | Body | When |
|---|---|---|
200 | text/html | The interactive redirect page |
404 | error envelope | No link with this code and org key |
410 | error envelope | Link deactivated, link expired, or organization suspended |
429 | error envelope | Over 500 requests/second from one IP |
The 200 never carries an HTTP redirect status. Routing happens inside the page: the server returns HTML tailored to the User-Agent, and the page or the OS moves the visitor to app, store, or web fallback. The full decision tree: Routing & fallbacks.
Response headers worth knowing:
| Header | Value | Meaning |
|---|---|---|
Cache-Control | no-store | Browsers get a fresh page every time; deferred tokens are single-use |
Cache-Control | public, max-age=60 | Link-preview crawlers get a static page, cached 60 seconds |
Accept-CH | Sec-CH-Timezone | Asks returning browsers for the timezone hint used in deferred matching |
If the link has deferred deep linking enabled, the page also carries a one-time match token (opl_…): the page copies it to the clipboard and, on Android, embeds it in the Play Store referrer, so it survives the install. The token lives for the link's match window.
Link previews
Known link-preview crawlers (WhatsApp, Facebook, X/Twitter, Telegram, LinkedIn, Slack, Discord, Skype previews, Snapchat, Pinterest, and others) get a static HTML page: OpenGraph and Twitter card tags, no JavaScript, no redirect, noindex. Social metadata falls back link → template → organization defaults.
Read link data
GET /:orgKey/:shortCode/data
When a Universal Link or App Link opens your app directly, the OS verifies the domain and skips the redirect page entirely. The SDK fetches this endpoint to turn the URL into link data:
{
"linkId": "b1c2d3e4-5f6a-7890-abcd-ef1234567890",
"path": "/product/123",
"params": { "utm_source": "newsletter", "color": "blue" }
}
path and params are the effective values: template defaults merged under the link's own overrides. An optional ?deviceId= query parameter registers the device with your organization for analytics.
Statuses match the redirect page exactly (200, 404, 410, 429); the success body is JSON instead of HTML. The open also counts as a click.
Match a first open
POST /match
Called by the mobile SDK on first app launch to recover the original click. No key is required. Sending your Mobile SDK key (opl_sdk_…) in an X-API-Key header attributes even failed matches to your organization.
Request body, all fields optional:
| Field | Type | Constraints | Purpose |
|---|---|---|---|
clipboardToken | string | max 100 chars | The opl_… token copied by the redirect page |
installReferrer | string | max 100 chars | Play Store install referrer token (Android) |
deviceId | string | 8–100 chars | Your stable device ID; registers the device |
platform | string | — | "iOS" or "Android" |
osVersion | string | — | e.g. "17.4.1" |
deviceModel | string | — | e.g. "iPhone16,1" |
language | string | — | e.g. "en-US" |
timezone | string | — | IANA name, e.g. "America/New_York" |
The backend tries signals in order and returns the strongest match: clipboard token, then install referrer (both deterministic), then exact fingerprint, then weighted scoring across device attributes, then same-IP as a last resort.
Matched:
{
"matched": true,
"matchMethod": "CLIPBOARD",
"matchConfidence": "exact",
"linkId": "b1c2d3e4-5f6a-7890-abcd-ef1234567890",
"path": "/product/123",
"params": { "color": "blue" }
}
| Field | Contents |
|---|---|
matchMethod | CLIPBOARD, INSTALL_REFERRER, FINGERPRINT_EXACT, FINGERPRINT_SCORED, IP_FUZZY, or NONE |
matchConfidence | exact, high, medium, low, or none |
matchScore | Present only for FINGERPRINT_SCORED; 0–100 weighted score |
No match is still a 200:
{ "matched": false, "matchMethod": "NONE" }
What the tiers mean for your numbers: Attribution & match confidence. Statuses: 200, 400 (validation), 429 (30 requests/minute per IP).
A matched token is single-use: after a successful match the record is consumed (with a 30-second grace period for SDK retries), so a second open can't match the same click twice.
Association files
Both files are served from every link domain and are scoped by the Host header. They are what makes OS-verified linking work; the portal's app configuration is their source. See Set up your organization.
| Endpoint | Serves | Missing configuration |
|---|---|---|
GET /.well-known/apple-app-site-association | Apple App Site Association JSON | 404 |
GET /.well-known/assetlinks.json | Android Digital Asset Links JSON | 404 |
On the OptoLink default domain, one file covers every organization: the AASA lists an appIDs entry per active organization with a complete iOS config, each scoped to /<orgKey>/*; assetlinks emits one statement per configured Android app. On a verified custom domain the files describe only that domain's organization. Both responses carry Cache-Control: public, max-age=3600, because Apple's and Google's CDNs re-fetch them periodically: expect up to an hour of delay after changing your app configuration.
Rate limits and caching on this path
Rate limits: 500 requests/second per IP for the redirect page and /data, 30 requests/minute per IP for /match. Details and the 429 body: Rate limits & errors. Caching: no-store for browsers (match tokens are single-use), 60 seconds for crawler pages, one hour for association files.