Make room for experiments.
Try your setup, adjust destinations, and work with your developer before a launch.
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.
You can start with a website destination and add mobile setup when you’re ready.
Sign up, verify your email, and open your dashboard.
Give it a name you’ll recognize. Follow the guided setup and enter the HTTPS website address visitors should reach.
Use something clear, like “Welcome offer.” If you’re linking to an app screen, add an app-relative path such as /products/42. Use / for the app’s starting destination.
Use the shareable URL LinkBud generates. Try it on the devices your audience uses and confirm the destination before sharing it publicly.
Every application has two environments. Check the environment selector before changing a destination or creating a link.
Try your setup, adjust destinations, and work with your developer before a launch.
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.
For browser visits, LinkBud chooses from the destinations saved in that environment.
| Visitor | Where the link sends them |
|---|---|
| iOS browser | Your App Store listing, if configured |
| Android browser | Your Google Play listing, if configured |
| Other visitors | Your website |
| A mobile platform without a store URL | Your 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.
Open Links inside the application and environment you’re working in.
Use search and the available filters to find a link. Copy its generated URL from LinkBud rather than building a URL from the application’s display name.
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.
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.
Register the Apple Team ID and Bundle ID. Your developer configures Associated Domains, Universal Link handling, and the app’s navigation.
Use the Flutter integration for Android and iOS. Both native platform identities and association setup are still required.
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.
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.
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.
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/v1/links | Create a link |
| GET | /api/v1/links/{id} | Read a link by its numeric record ID |
| GET | /api/v1/sources | List saved source labels |
| GET | /api/v1/campaigns | List 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.
Open Activity and choose your date window. Narrow the view by platform, link, source, or campaign. Check the selected environment first.
App-reported metrics need a working SDK integration. A browser-only link can produce clicks and store redirects without producing app navigation reports.
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.
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.
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.
Testing and Live are separate. Open Live and configure it independently. Test a link created in that environment before sharing it.
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.
Tell us what you expected, what happened, and which platform you tested. Keep API keys, passwords, and private destination details out of your message.
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.
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.
resume() to revalidate; do not navigate from cached pending() state.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.
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.
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.
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>
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)
}
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.
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.
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.
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.
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()
| 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.
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.
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.
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.
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.
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.
| 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.
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.
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.
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-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.
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 | 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.
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.
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.