Skip to documentation
Explore the guide
Bring LinkBud into your workflow.

Copy versioned setup, SDK examples, API contracts, and acceptance checks for Claude, Codex, or your preferred LLM.

Integration

A CLEAR PATH FROM YOUR FIRST LINK

Start here

Welcome to LinkBud. Bring your app’s destinations together, test before you share, and see what happens next. Start with one link. Build from there.

The idea is simple. An application is the app you’re working on. An environment is where you configure and test its links. A link is the URL you share with your audience.

02 / Testing & Live

Every application has two environments. Check the environment selector before changing a destination or creating a link.

TESTING

Make room for experiments.

Try your setup, adjust destinations, and work with your developer before a launch.

LIVE

Prepare your public links.

Configure the destinations and platform identities you want your audience to use, then create and verify your Live links.

Links, destinations, mobile identities, and credentials stay with the selected environment. A change in Testing does not change Live. Set up Live separately; switching environments does not copy or promote a link.

03 / Where visitors go

For browser visits, LinkBud chooses from the destinations saved in that environment.

Browser redirect destinations
VisitorWhere the link sends them
iOS browserYour App Store listing, if configured
Android browserYour Google Play listing, if configured
Other visitorsYour website
A mobile platform without a store URLYour website

Configure these in the application’s Setup area. Keep a working HTTPS website fallback even when your main destination is an app.

The app screen path, such as /products/42, is a navigation hint for an integrated app. It does not replace the website URL or automatically get added to the website fallback.

A store redirect isn’t app-opening detection. Installed-app opening depends on Android App Links or iOS Universal Links, a correctly signed app, and its integration. Always test on an actual device.

05 / Connect your app

In Setup, choose your integration: Android Native, iOS Native, both native platforms, or Flutter. Use the environment’s generated configuration and developer handoff to keep everyone working with the same values.

Android

Kotlin preview

Register the package name and SHA-256 app-signing fingerprint. Match the Play listing to that package, then configure App Links using the generated setup.

iOS

Swift preview

Register the Apple Team ID and Bundle ID. Your developer configures Associated Domains, Universal Link handling, and the app’s navigation.

Flutter

Dart preview

Use the Flutter integration for Android and iOS. Both native platform identities and association setup are still required.

The app’s part of the journey

  1. Receive and resolve the incoming link using the SDK.
  2. If sign-in is needed, keep the pending destination and resume after authentication.
  3. Check that the path is supported and the user is allowed to open it.
  4. Navigate, then acknowledge the destination after navigation succeeds.
Preview availability: Android, iOS, and Flutter SDKs are local, unreleased previews. Public package-registry releases and signed-device, real-store-install, and real-authentication acceptance are not complete. Use the supplied preview materials with your developer; do not assume a public package install is available.
What happens if the app isn’t installed?

The browser can send the visitor to the configured store listing. Android’s preview supports Play Install Referrer recovery when correctly integrated. iOS recovery uses an explicit recovery code; it is not automatic. Neither route is a guarantee that a new installation will recover a destination. Verify the complete installation journey before relying on it.

Keep server API keys and management credentials on your backend. Mobile apps use the environment’s public SDK identifiers.

06 / Server API

Your backend can create and read links using an environment-scoped server API key. Open Settings → Server API keys for the selected environment. Keep the key secret and copy the API origin from your setup.

EXAMPLE · RUN ON YOUR SERVER
curl --request POST "$LINKBUD_ORIGIN/api/v1/links" \
  --header "Authorization: Bearer $LINKBUD_SERVER_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: welcome-offer-001" \
  --data '{"title":"Welcome offer","destination":"/welcome"}'

Set LINKBUD_ORIGIN to your configured link origin and LINKBUD_SERVER_KEY to the key for the intended environment. These names are example shell variables, not credentials. Use a new idempotency key for each new link; reuse the same key and payload when retrying the same creation.

Available server endpoints
MethodEndpointPurpose
POST/api/v1/linksCreate a link
GET/api/v1/links/{id}Read a link by its numeric record ID
GET/api/v1/sourcesList saved source labels
GET/api/v1/campaignsList saved campaign labels

For tracking, send source_id and campaign_id from the label endpoints, not label names. Optional expires_at uses an ISO 8601 time with a timezone. The key selects the environment; do not include ownership or environment fields in the payload.

A validation error means a field needs attention. An authentication error means you should check the key, its abilities, and the configured origin. A rate-limit response means slow down and retry later. Reusing an idempotency key with a different payload produces a conflict.

07 / Understand activity

Open Activity and choose your date window. Narrow the view by platform, link, source, or campaign. Check the selected environment first.

Clicks
Eligible browser visits that return a redirect. Repeat visits can count again.
Store redirects
Visits sent to a store listing. These are not download or installation counts.
App opens
SDK resolutions that deliver a route hint. They do not prove the destination screen opened.
Destinations opened
Successful navigation reported by the integrated host app.
Routing success
The share of receipt-tracked journeys started in the period that report successful navigation. Missing reports remain incomplete.
Time to destination
Average SDK-reported journey time until the app confirms navigation. It includes sign-in and excludes time spent in the store.

App-reported metrics need a working SDK integration. A browser-only link can produce clicks and store redirects without producing app navigation reports.

08 / Troubleshooting

My link goes to the website instead of the store.

Open the same environment the link belongs to and check its platform store URL. A missing store URL uses the website fallback. Also test in a normal browser on the intended mobile platform; embedded browsers and desktop-mode devices may behave differently.

The link says it’s unavailable.

Check whether the link is disabled or expired. Application or account restrictions can also stop links resolving. Confirm you copied the current URL from the correct environment.

The installed app doesn’t open.

Check the signed app’s identity against Setup, its App Links or Universal Links configuration, and the generated association document. Then confirm the app handles the incoming URL. Browser redirects alone do not configure installed-app opening.

I changed Testing, but my public link didn’t change.

Testing and Live are separate. Open Live and configure it independently. Test a link created in that environment before sharing it.

Clicks appear, but app metrics don’t.

Check that the SDK uses the same application and environment as the link. Confirm your app handles the route and acknowledges only after successful navigation. A store redirect does not prove an installation or app screen visit.

Still finding your way?

Tell us what you expected, what happened, and which platform you tested. Keep API keys, passwords, and private destination details out of your message.

Contact us

09 / Integration checklist

What to give your developer or LLM

Reference version: LinkBud SDK 0.4.0 preview / server API v1. These instructions describe the implementation in this project. SDKs are not published to public package registries. Obtain the matching preview source/package from your LinkBud team before starting. A developer handoff contains public configuration, not the SDK source. Do not invent a download URL or assume a package is on Maven Central, pub.dev, or a remote Swift repository.

Supply this reference, the matching SDK source, access to your app/backend repository, and the public configuration from Setup → developer handoff. Server secrets belong in your backend environment configuration and must not be pasted into a shared LLM chat. The values below are examples to replace, not working credentials.

Input Where it comes from Where it belongs
Link origin Generated setup; root HTTPS origin Mobile app and backend
Application public ID Developer handoff; 26 lowercase letters/digits Mobile configuration
Environment Exactly testing or live Mobile and backend context checks
Environment SDK ID Developer handoff; 26 lowercase letters/digits Mobile configuration
Route prefix Settings/handoff; three case-sensitive letters/digits Mobile configuration and OS association paths
Android package and SHA-256 Actual signed build; save in LinkBud Setup Android setup and association document
Apple Team ID and Bundle ID Actual signed app; save in LinkBud Setup iOS setup and SDK appleAppID/appleAppId
Server API key Settings → Server API keys Backend secrets only
Test link Copy a generated Testing link Device acceptance testing

Origins must use HTTPS with no credentials, custom port, query, fragment, or path. Use separate app identities/build flavors for Testing and Live. Never switch to another environment to recover a failed link.

Implementation instructions

Inspect the host repository before editing. Find its real login flow, signed-in state, route parser, authorization checks and navigation completion signal. Ask for missing SDK artifacts, public configuration, supported destinations, or authentication/navigation hooks. Do not replace them with simulated login or claim integration is complete because a sample compiles.

  1. Install the matching preview and configure OS association separately for each platform.
  2. Keep one long-lived SDK client per configuration. Forward cold starts and warm links exactly once.
  3. Treat pending destinations as untrusted hints. If signed out, present the app’s real login and retain the pending journey. After login, call resume() to revalidate; do not navigate from cached pending() state.
  4. Allowlist the route, authorize its resource with the host backend, navigate, then acknowledge the delivery ID only after successful navigation. Check the acknowledgment result.
  5. Serialize operations and navigation by delivery ID. Make host navigation idempotent: a process can die after navigation but before acknowledgment.
  6. Implement retry, expiry and failure UI, then run the acceptance checklist below on signed devices.

The SDK retains one journey and bounded acknowledgment history (64 deliveries). A different link cannot overwrite an outstanding journey. Pending state survives app restarts/sign-in, but not uninstalling or clearing app data. Direct input expires within 24 hours or earlier server/link expiry; iOS code input within 30 minutes or earlier expiry.

Recovery and reporting boundaries

Android Play recovery depends on the Play Install Referrer integration and an unexpired handoff. Non-Play installs must reopen the original link. iOS uses an explicit code requested on LinkBud’s continuation page and explicitly entered/pasted by the user; there is no automatic App Store payload transfer or silent clipboard reading. Copy continuation URLs from LinkBud rather than constructing legacy or short URL shapes.

Acknowledgment queues a best-effort destination-open report. For a permanently abandoned journey, destinationFailed accepts navigation, authentication, permission, unavailable, or unsupported. Do not mark a journey failed simply because sign-in is still in progress. Reports are bounded/expiring and do not prove a download, unique person, or business conversion. Never log keys, recovery codes, tokens, installation IDs or private destination parameters.

10 / Android reference

Install the local preview

The Kotlin preview requires Android API 26+, JDK 17 and compile SDK 36. The checked project uses Gradle 8.13, AGP 8.13.2 and Kotlin 2.2.21. It depends on Play Install Referrer 2.2. The import namespace is dev.linkbud.sdk.

With the supplied source repository, publish to your local Maven repository:

cd android-sdk
./gradlew :sdk:testDebugUnitTest :sdk:assembleRelease :sdk:publishToMavenLocal

In the consuming host’s dependency repositories add mavenLocal(), then its app module:

dependencies {
    implementation("dev.linkbud:android-sdk:0.4.0")
}

This coordinate works after local publishing; it is not an instruction to download from a public registry. Keep the matching source/artifact in your build process for CI. Do not copy an AAR alone and omit its transitive Play dependency.

Configure App Links

Save the app package and actual signing SHA-256 in the selected environment. For Play-distributed builds use the Play app signing certificate, not an upload/debug certificate. Set the Play URL’s id to the same package. The generated /.well-known/assetlinks.json must match that package/certificate over public HTTPS.

The following manifest example belongs inside your actual exported receiving Activity. Replace the host/prefix; retain your launcher intent filter separately. Before short-link cutover, also use the generated legacy environment filters shown in Setup. Do not invent broad host-only filters or guess which URL namespace is active.

<activity android:name=".MainActivity"
    android:exported="true" android:launchMode="singleTop">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="links.example.com"
            android:pathPrefix="/Ab1/" />
    </intent-filter>
</activity>

Keep one client and receive links

Own this instance in your Application or equivalent process-wide container, not in each Activity. Replace every example identifier with handoff values; Ab1 is illustrative.

import dev.linkbud.sdk.LinkBud
import dev.linkbud.sdk.LinkBudConfig
import dev.linkbud.sdk.LinkBudResult

val linkbud = LinkBud(applicationContext, LinkBudConfig(
    origin = "https://links.example.com",
    applicationId = "YOUR_APPLICATION_PUBLIC_ID",
    environment = "testing",
    sdkId = "YOUR_ENVIRONMENT_SDK_ID",
    routePrefix = "Ab1",
))

The next snippets are Activity integration points using that shared client and your onLinkBudResult handler. Keep your existing super.onCreate(...) and Activity setup.

// During onCreate, after Activity initialization:
if (intent?.action == android.content.Intent.ACTION_VIEW) {
    linkbud.handleIntent(intent, ::onLinkBudResult)
} else {
    linkbud.resume(::onLinkBudResult)
}

// Inside the receiving Activity:
override fun onNewIntent(intent: android.content.Intent) {
    super.onNewIntent(intent)
    setIntent(intent)
    linkbud.handleIntent(intent, ::onLinkBudResult)
}

Handle every result

The SDK callback runs on the main thread. Implement the following branches in your app’s handler; UI/navigation callbacks are host responsibilities, not SDK methods.

Kotlin result Required host behavior
LinkBudResult.Pending Read result.value.id, .destination, .expiresAt. Wait for real login if needed; call resume(::onLinkBudResult) after login. Authorize/navigate before acknowledging.
LinkBudResult.Empty No usable journey; show normal app entry.
LinkBudResult.AlreadyAcknowledged Do not navigate again.
LinkBudResult.Retryable Preserve input; offer resume at/after result.retryAt on user/connectivity/lifecycle action.
LinkBudResult.Rejected Explain unavailability safely; use result.reason for non-sensitive diagnostics. Never invent a destination.

Only after the host confirms authorized navigation:

linkbud.acknowledge(pending.id) { saved ->
    // The screen is already open. If false, retain safe host deduplication
    // and show a recoverable status instead of opening it a second time.
    onAcknowledgmentSaved(saved)
}

pending and onAcknowledgmentSaved above are the host’s retained PendingDestination and completion handler. They are not global SDK variables. Use destinationFailed(pending.id, "permission", completion) only when that journey is abandoned, not while waiting for login.

Network failures/429/5xx are retryable. Backoff begins at 2 seconds, caps at 5 minutes, and bounded numeric Retry-After can extend it. 401/404/409/410/422, redirects and malformed payloads are terminal. pending_destination_exists preserves the first journey. local_state_failure must fail closed. There is no background polling; non-Play recovery needs original-link reopening.

Verify Android association

Replace the example package and URL with the actual signed build and a generated Testing link.

adb shell pm verify-app-links --re-verify com.example.app
adb shell pm get-app-links com.example.app
adb shell am start -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE -d 'YOUR_GENERATED_TEST_LINK'

Test cold/warm entry, real login, process restart during login, offline retry and disabled/expired links. A debug install does not prove fresh Play-install recovery. Signed-device and real Play-install acceptance are still required before declaring the preview production-ready.

11 / iOS reference

Install and identify the app

The Swift package is a local 0.4.0 preview requiring iOS 15+ and Swift tools 5.9. In Xcode, add the supplied ios-sdk directory as a local package and link its LinkBud product. No public package URL is available. Obtain matching source before changing the host app. macOS support is a Foundation test harness, not a desktop app-opening integration.

Register the signed build’s Team ID and Bundle ID in the correct environment. Enable Associated Domains in the target’s signing/provisioning setup and add this entitlement, replacing the host:

<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:links.example.com</string>
</array>

The generated /.well-known/apple-app-site-association must include the actual TEAMID.bundle.id and exact generated environment paths. Do not add HTTPS or a path to the entitlement value. Check both the signed entitlements and the publicly served association document.

Initialize a long-lived client

import Foundation
import LinkBud

let configuration = try LinkBudConfiguration(
    origin: URL(string: "https://links.example.com")!,
    applicationID: "YOUR_APPLICATION_PUBLIC_ID",
    environment: "testing",
    sdkID: "YOUR_ENVIRONMENT_SDK_ID",
    appleAppID: "A1B2C3D4E5.com.example.app",
    routePrefix: "Ab1"
)
let linkbud = try LinkBud(configuration: configuration)

Replace all illustrative values. Initialization can throw for invalid configuration or unavailable/corrupt local storage; show a recoverable error instead of navigating. Keep one client per configuration in the host model. Call asynchronous operations serially and update UI on the main actor.

Forward cold and warm Universal Links

For SwiftUI, use .onOpenURL and browsing-web .onContinueUserActivity with activity.webpageURL. For UIKit scenes, handle both connectionOptions.userActivities during connection and scene(_:continue:). For a non-scene app use application(_:continue:restorationHandler:). Route those events through one serialized host coordinator and call:

let result = await linkbud.handle(url: incomingURL)

incomingURL is the URL supplied by the OS. Do not forward the same event through duplicate handlers. If an operation is busy, retain one URL in your coordinator and process it after completion, or visibly ask the user to reopen it; do not silently discard it. Resume persisted input when the app starts normally:

let result = await linkbud.resume()

Route after real authentication

Swift result Required host behavior
.pending(let pending) Inspect id, destination, expiresAt. Retain while showing real login; revalidate with resume() afterward, then allowlist/authorize/navigate.
.empty Normal app entry; no automatic install lookup happens.
.alreadyAcknowledged Do not navigate again.
.retryable(let retryAt) Retry resume() no earlier than that time, from a visible or lifecycle/connectivity trigger.
.rejected(let reason) Show a safe unavailable/reopen/recovery message; do not route.

After navigation has actually succeeded:

let saved = await linkbud.acknowledge(id: pending.id)

Here pending is the value returned by .pending, not an SDK global. If saving fails, preserve host-level idempotency and report the failure. Cached pending() is for displaying waiting state, not a substitute for revalidation. A different incoming journey cannot replace the outstanding one. There is no transaction spanning host navigation and acknowledgment.

Explicit installation recovery

Offer a clearly labeled recovery-code field with a user-initiated submit action. Copy the continuation page URL from LinkBud; do not construct it from display names or a guessed URL shape. The user requests a code there, installs/opens the app, and explicitly enters/pastes the code. Then call:

let result = await linkbud.recover(code: enteredCode)

enteredCode is explicit user input. Codes are 24 hexadecimal characters, shown in six groups of four; lowercase/display hyphens and surrounding whitespace are normalized. They expire after 30 minutes or earlier link expiry. Code claims are bound to the environment, Apple identity and installation; another installation cannot reuse a claimed code. Use the same authentication, revalidation and authorization flow as a direct link.

Never read the clipboard silently. Do not promise automatic App Store deferred recovery. Invalid/expired/claimed codes, disabled links or canceled installs must produce a clear retry/reopen path without fabricated navigation.

Errors and device acceptance

Offline/429/5xx are retryable with bounded backoff. 401/404/409/410/422, redirects and invalid payloads are terminal. Handle operation_in_progress by serializing host operations; preserve pending work on conflicts. A storage failure must fail closed until repaired/reinitialized. No background polling is provided.

Build with your real signing team and verify cold/warm links on a physical signed iPhone, real login, restart during login, and user-entered recovery after an actual install. Simulator builds and unit tests do not establish Universal Link association or store-install acceptance. These acceptance checks remain open for the preview release.

12 / Flutter reference

Prepare the local plugin

Package: linkbud_flutter 0.4.0 preview; not published on pub.dev. The package requires Flutter >=3.35 and Dart >=3.9 <4. Use Android API 26+ and iOS 15+. Native Android setup uses JDK 17 / compile SDK 36; iOS needs Xcode/signing and the checked CocoaPods workflow. Obtain the complete preview repository/package from the LinkBud team first.

From the LinkBud root containing the separate SDK checkouts:

python3 flutter-sdk/tool/prepare_native.py --verify
cd flutter-sdk
flutter pub get
flutter test
flutter analyze

The package includes tracked native source snapshots. --verify checks their hashes without needing native checkouts. Maintainers refresh snapshots with prepare_native.py after native SDK changes; do not edit snapshots directly. A prepared plugin package is self-contained. In your host pubspec, use a local path appropriate to where you placed the prepared package:

dependencies:
  flutter:
    sdk: flutter
  linkbud_flutter:
    path: ../flutter-sdk

That path is illustrative and must exist locally; it is not a registry coordinate. Run flutter pub get in the host. Complete both Android and iOS association configuration from the native guides above, using distinct app identities for Testing and Live.

Avoid competing link handlers

Disable Flutter’s built-in deep-link router for this integration. Inside the Android receiving Activity:

<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />

In iOS Info.plist:

<key>FlutterDeepLinkingEnabled</key>
<false/>

For Flutter, use the native guides for identities, intent filters and entitlements only. Do not initialize additional Kotlin/Swift clients: the plugin owns its native SDK instances.

Retain the Android autoVerify intent filter and iOS Associated Domains entitlements from the platform guides. The plugin receives native OS events; do not also forward the same callbacks through a second router. handleUrl(url) is for host-managed explicit entry points.

Subscribe before initialization

The following uses real plugin API names. onLinkBudResult and onBridgeError are handlers you implement in the app’s long-lived integration coordinator. yourApplicationId, yourSdkId, yourRoutePrefix and yourAppleAppId come from its public configuration; they are not package variables.

import 'dart:async';
import 'package:linkbud_flutter/linkbud_flutter.dart';

final linkbud = LinkBud.instance;
final subscription = linkbud.incoming.listen(
  onLinkBudResult,
  onError: onBridgeError,
);
final initial = await linkbud.initialize(LinkBudConfiguration(
  origin: 'https://links.example.com',
  applicationId: yourApplicationId,
  environment: 'testing',
  sdkId: yourSdkId,
  routePrefix: yourRoutePrefix,
  appleAppId: yourAppleAppId,
));
onLinkBudResult(initial);

Keep the subscription for the integration’s lifetime and cancel it when that coordinator is disposed. Initialize once in the main isolate, after Flutter bindings are initialized. There is one supported Flutter engine/configuration per app process. Initialization errors must be shown, not converted into fake destinations.

Handle the lifecycle

Dart result/API Meaning and next step
PendingDestination Contains id, destination, expiresAt; retain for real login, revalidate, authorize and navigate.
LinkBudEmpty Normal app entry, or offer reopening/manual iOS recovery.
LinkBudAlreadyAcknowledged Do not navigate again.
LinkBudRetryable Use .retryAt; retry from a user/connectivity/lifecycle action at/after that time.
LinkBudRejected Use .reason for safe status; no navigation.
PlatformException / FormatException Bridge/configuration/engine/contract failure; show a recoverable error and fail closed.

After real login or a missed stream event, revalidate native state:

onLinkBudResult(await linkbud.resume());

After the host has successfully opened an authorized, allowlisted destination:

final saved = await linkbud.acknowledge(pending.id);

pending is the retained PendingDestination. Check the Boolean saved; prevent duplicate navigation using the host’s own in-flight/completed-delivery handling. Do not acknowledge just because you displayed login. pending() only inspects cached state.

On iOS only, a labeled user-submitted recovery form calls:

onLinkBudResult(await linkbud.recover(enteredCode));

Android returns unsupported_recovery for that operation and uses Play referrer recovery through resume() instead. No silent clipboard access or automatic iOS install recovery is provided.

Serialize operations. Handle operation_in_progress, operation_in_progress_reopen_link, pending_destination_exists, engine_in_use and configuration_locked without replacing or losing the first journey. Use separate build flavors/process restarts for configuration changes. There is no background polling, web/desktop integration or supported multi-engine mode in this preview.

Test both native platforms on signed devices, including real authentication, process restart while waiting for login, cold/warm links, offline retry and platform-specific install recovery. Passing Dart tests alone is not end-to-end acceptance.

13 / Backend reference

Credentials and scope

Use a server key from the intended application environment. Send Authorization: Bearer YOUR_SERVER_API_KEY and Accept: application/json. Never embed the key in mobile builds, frontend JavaScript, URLs, logs, examples committed to source, or shared LLM transcripts. Keys do not expire automatically; revocation, ability scope and parent account/application availability remain enforced. Replace/revoke through Settings when rotating.

The key chooses ownership/application/environment. Do not accept a customer-selected ownership value as authority. Optional context assertions help catch a misconfigured key:

X-LinkBud-Expected-Application: YOUR_APPLICATION_PUBLIC_ID
X-LinkBud-Expected-Environment: testing

An assertion mismatch returns 403 integration_context_mismatch. Supported key abilities are links:create, links:read, labels:read. Store the secret in your backend’s secret configuration and the generated link ID/URL in your own application data under your own authorized owner.

Create a link

POST /api/v1/links, on the configured link origin. Content-Type must be application/json.

Field Rule
title Required string, maximum 100 characters.
destination Required string, maximum 2048 characters; app-relative path such as /products/42. Decoded path must begin with one slash, not two; full URLs, spaces/control characters and backslashes are rejected. Treat all parameters as untrusted.
expires_at Optional/null ISO 8601 string with timezone, future time no later than 2038-01-19T03:14:07Z.
source_id Optional/null positive integer for an active saved source owned by the key’s user.
campaign_id Optional/null positive integer for an active saved campaign owned by the key’s user.

Ownership/environment fields, slug, enabled, url, web_url, label names (source, campaign), and created_via cannot be supplied. Use numeric label IDs from the listing endpoints. A new API link is enabled and its origin is recorded as API.

{
  "title": "Welcome offer",
  "destination": "/welcome",
  "source_id": 12,
  "campaign_id": 34,
  "expires_at": "2030-01-01T12:00:00Z"
}

Source/campaign IDs above are examples: omit them until you have fetched real active IDs. Omit expiry for a non-expiring link. Successful creation returns 201, with this response shape (values are illustrative):

{
  "id": 42,
  "url": "THE_GENERATED_SHAREABLE_URL",
  "title": "Welcome offer",
  "destination": "/welcome",
  "application_id": "YOUR_APPLICATION_PUBLIC_ID",
  "environment": "testing",
  "enabled": true,
  "expires_at": null,
  "source": null,
  "campaign": null,
  "created_at": "2026-11-04T12:00:00.000000Z"
}

The response example uses a request with labels/expiry omitted. Use returned url verbatim. Do not infer URL shapes from id (numeric record ID) or application_id (public ID), and do not reconstruct a URL from a display name. The rollout may use short or legacy URLs; the API response is authoritative.

Backend client example

This is a Node.js 18+ server-side helper. It performs one attempt and leaves retry scheduling to the host’s durable job system. Persist operationId and the payload before the first attempt. On network failure or a retryable status, rerun with the same operation ID and payload; do not generate a new ID inside the retry loop.

export async function createLinkBudLink({
  origin, serverKey, applicationId, environment, operationId, payload,
}) {
  const base = new URL(origin);
  if (base.protocol !== 'https:' || base.username || base.password ||
      base.port || base.search || base.hash || !['', '/'].includes(base.pathname)) {
    throw new Error('Invalid configured LinkBud origin');
  }
  const response = await fetch(new URL('/api/v1/links', base), {
    method: 'POST',
    redirect: 'error',
    signal: AbortSignal.timeout(15000),
    headers: {
      Authorization: `Bearer ${serverKey}`,
      Accept: 'application/json',
      'Content-Type': 'application/json',
      'Idempotency-Key': operationId,
      'X-LinkBud-Expected-Application': applicationId,
      'X-LinkBud-Expected-Environment': environment,
    },
    body: JSON.stringify(payload),
  });
  const body = await response.json().catch(() => null);
  if (response.status !== 201) {
    const error = new Error('LinkBud creation failed');
    error.status = response.status;
    error.code = body?.error ?? 'request_failed';
    error.retryAfter = response.headers.get('Retry-After');
    error.fields = response.status === 422 ? body?.errors : undefined;
    throw error;
  }
  if (!body || !Number.isInteger(body.id) || typeof body.url !== 'string') {
    throw new Error('Unexpected LinkBud response');
  }
  return body;
}

Read origin, serverKey, applicationId and environment from trusted server configuration, not browser-submitted parameters. Before calling, authenticate your own user and authorize the business resource represented by the destination. On success, persist id and url for that authorized resource. Do not log the complete exception object, authorization header, request body or sensitive validation values.

Idempotency and retries

Idempotency-Key is optional but strongly recommended: 1–128 letters, digits, dots, underscores, colons or hyphens. Reuse it only for the same logical creation and same payload. Successful responses are replayable for 24 hours within the environment. A replay returns 201 and Idempotency-Replayed: true; the first stored response uses false. Different payload under that key returns 409 idempotency_conflict. If the original link is now unavailable, replay returns 410 link_unavailable.

After the 24-hour replay window, retrying can create another link; reconcile with your stored link ID before retrying old work. Network errors can happen after creation, so do not assume a timeout means no link exists. Retry transport errors, 429 and 5xx with bounded attempts/backoff and honor Retry-After. Do not automatically retry validation/authentication/context conflicts with an unchanged bad request.

Read a link and list labels

GET /api/v1/links/{id} requires links:read and the numeric record ID. It returns 200 with the same payload shape as creation, including disabled/expired records. Reading a record does not prove its public URL currently resolves. Unknown or another environment’s record returns 404. There are no public server-API edit/delete/list-links endpoints in this version; link editing and disabling are dashboard operations.

GET /api/v1/sources and /api/v1/campaigns require labels:read. page is an integer >=1; per_page is 1–100, default 50. Active labels are ordered by name then ID, scoped to the key owner.

{
  "data": [{"id": 12, "name": "Newsletter"}],
  "meta": {"page": 1, "per_page": 50, "total": 1, "last_page": 1}
}

HTTP error contract

HTTP JSON / meaning Host action
401 {"error":"invalid_credentials"} Check secret format/revocation; do not retry indefinitely.
403 integration_context_mismatch, integration_unavailable, insufficient_scope Check configured application/environment, key abilities and account/application availability.
404 Record not found (Laravel JSON message) Check numeric ID and key environment; no cross-environment fallback.
409 idempotency_conflict Reconcile original operation; do not overwrite it with a different payload.
410 link_unavailable on replay Do not reuse an unavailable link as a success.
415 json_required Send Content-Type application/json and a JSON body.
422 Laravel message and errors object Surface field errors; fix the payload.
429 rate_limited Honor Retry-After and apply bounded retry.
5xx / transport failure Service/network failure; response may be non-JSON Preserve operation/payload and retry safely; do not log secrets.

A validation response looks like {"message":"…","errors":{"title":["…"]}}; text may vary, so use HTTP status and field keys. Default quotas are 300 requests/minute/IP; creation 60/minute/key and 120/minute/environment; reads/labels 120/minute/key and 240/minute/environment. Successful authenticated requests must respect both key and environment quotas. Never use a mobile SDK identifier as a server API key.

14 / Verify your integration

Inputs to confirm before implementation

Ask the app owner for the target platform/framework and minimum OS versions, matching preview artifacts, actual public handoff configuration, a generated Testing link, supported route patterns, real authentication entry/resume hooks, authorization service and navigation completion callback. Ask the backend owner for its stack, durable job mechanism, trusted environment configuration, authorized resource model and intended server-key abilities. Secrets should be configured directly in the host’s secret store.

If anything is missing, identify the exact missing input and stop the dependent part. Do not guess IDs, signing identities, origin, SDK package URLs, current public link namespace, or the host’s authorization rules. The generic public guide cannot supply app-specific business logic or credentials.

Verify the backend

  1. Create a Testing link with a real authorized app path; store its numeric ID and returned URL.
  2. Repeat with the same operation ID/payload and verify the same response/link. Change the payload and verify a conflict.
  3. Check invalid/revoked key, wrong expected environment, insufficient ability, invalid destination, another owner’s label and another environment’s link ID.
  4. Test timeout-after-creation safely using the same idempotency key, 429 handling, bounded retries and expired replay reconciliation.
  5. Check that no server key reaches the mobile bundle, browser, logs or committed source.

Verify each mobile platform

  1. Open the generated link with the app closed, already running, and backgrounded. Confirm one authorized navigation, not duplicate screens.
  2. Open while signed out, complete the real login and verify resume/revalidation reaches the intended destination. Restart the process during login and repeat.
  3. Disable/expire the link while login is pending; resumption must not open it. Submit an unsupported path and a resource the user cannot access; both must fail safely.
  4. Test offline input, connectivity return and server retry time. Preserve the pending journey; do not acknowledge or loop without respecting backoff.
  5. Open a second distinct link while one is pending. Verify the first is not silently replaced. Test a crash/restart around navigation/acknowledgment without duplicate destructive side effects.
  6. Android: verify associations for the actual signing certificate and test a real fresh Play install/referrer journey. Also test a non-Play install with original-link reopening.
  7. iOS: verify signed entitlements/association on a physical device, then explicitly request/enter a recovery code after installation. Check expired/mistyped/claimed codes and confirm no silent clipboard read or automatic App Store recovery is assumed.
  8. Flutter: run the checks on both Android and iOS with the built-in Flutter router disabled. Verify warm events, restart recovery, operation serialization and subscription lifetime.
  9. Confirm activity distinguishes browser clicks/store redirects from SDK resolutions and actual navigation reports. A store redirect must not be presented as an installation.

Handoff completion report

Report changed host files, installed SDK version/artifact source, environment tested, route/authentication hooks wired, tests actually run, device/signing/store-install evidence and remaining limitations. Separate verified behavior from source preparation. The 0.4.0 SDKs remain unreleased previews with signed-device, real-install and real-authentication acceptance open until those checks are actually performed for your integration.