Skip to main content
Documentation
API Reference

Apps

Apps API endpoints to register and manage the mobile apps used for deep linking, with iOS and Android configuration, verification files, and plan limits.

An app is the piece of WarpLink's data model between an organization and a link: organization → apps → links. Register an app before you create links: a request to create a link in an organization with no app is refused with APP_REQUIRED. A link can still point only to a web page. Links that already have no app_id keep resolving a destination, tracking clicks, and rendering social previews, but they have nothing to open a native app with. Attach an app, and a link gains Universal Links on iOS, App Links on Android, and an entry in the install attribution match cascade. Most organizations register exactly one app that covers both platforms; the API doesn't require that shape, but the dashboard's UI is built around it.

An app holds independent, optional configuration for iOS and Android. You can register just one platform, both, or add the second one later with an update. The only hard requirement, enforced on both create and update, is that at least one platform stays configured: an app with neither an ios_bundle_id nor an android_package_name is refused, because an app with no platform can't do the one thing an app exists to do.

Authentication and Permissions

OperationScopeRole required (dashboard-created keys)
Create an appapps:writeany role, including member (apps:write)
List appsapps:readany role, including member (apps:read)
Get an appapps:readany role, including member (apps:read)
Update an appapps:writeany role, including member (apps:write)
Delete an appapps:writeowner or admin (apps:delete)

Reading, creating, and updating apps is open to every role, including member, which matches the dashboard's own permission matrix: anyone on the team can register an app and edit how it is wired up. Deleting an app needs the apps:write scope and a role that holds the apps:delete permission, which only owner and admin roles hold, so a key whose creator is a member gets 403 FORBIDDEN on delete. The role check only applies to keys created through the dashboard UI, which carry a created_by; a key created without that context is authorized on scope alone. See Authentication for how the two layers combine on every request.

Plan Limits

Apps count against a per-organization limit that scales with plan:

PlanApp limit
Free1
Starter5
Growth20
ScaleUnlimited
UnlimitedUnlimited

One "app" here covers every platform you register under it. iOS, Android, React Native, and Flutter configuration for the same product all live on one app row, so a cross-platform app that supports both stores still counts once. The limit is checked before validating the request body: creating past the limit fails with 403 APP_LIMIT_EXCEEDED regardless of whether the body itself would have been valid.

Validation and Behavior

  • ios_bundle_id must be reverse-domain notation (for example com.example.myapp), up to 255 characters.
  • ios_team_id must be alphanumeric, uppercase, up to 20 characters.
  • ios_app_store_id must be numeric, up to 20 characters.
  • android_package_name must be a valid Java package name (for example com.example.myapp), up to 255 characters.
  • android_sha256_fingerprints are normalized to uppercase and validated as 32 colon-separated hex byte pairs each.
  • ios_universal_link_paths and android_app_links_paths are arrays of paths, each starting with / (for example /products/*).
  • probabilistic_matching is a boolean, true by default. Set it to false for routing-only mode: deep links keep working, and the server no longer matches installs by probabilistic match or raw device signals for that app, so deferred links and attribution use only the Play Install Referrer and the device ID repeat check. It is on update only, applies at once, and any role that can update the app can change it. See Routing-only mode.
  • org_id cannot be set on update. Sending it, even unchanged, is refused with 400 IMMUTABLE_FIELD before the rest of the body is even validated.
  • A bundle ID or package name must be unique within its organization. A collision, on create or on update, is refused with 409 DUPLICATE_APP.
  • Updating an app's identifiers or paths regenerates the app's apple-app-site-association and Android assetlinks.json files automatically, so Universal Links and App Links keep working without a separate publish step. This runs after the response is sent and never blocks or fails the update itself.
  • The app's icon is resolved automatically from the store IDs you provide (App Store ID, Play package name) shortly after creation. This also runs after the response and never blocks it; icon_url and icon_source on the app may still be null immediately after a 201.
  • Deleting an app does not delete its links. A link's app_id is a nullable foreign key that clears to null on app delete (onDelete: "set null"), so the link and its click history survive as a web-only link with no app. The app's affected links are automatically resynced afterward so nothing keeps pointing at deep link configuration that no longer exists.

Create an App

POST/apps

Create an app

Request Body

FieldTypeDescription
name*string
ios_bundle_idstringReverse-domain notation, e.g. com.example.myapp
ios_team_idstringApple Team ID, e.g. ABCDE12345
ios_app_store_idstringNumeric App Store ID
ios_app_store_urlstring
ios_universal_link_pathsstring[]
android_package_namestringJava package name, e.g. com.example.myapp
android_sha256_fingerprintsstring[]
android_play_store_urlstring
android_app_links_pathsstring[]
link_handlestringThe app's link subdomain, as in link_handle.aplnk.to. 3 to 30 characters: lowercase letters, numbers and hyphens, not starting or ending with a hyphen. Reserved names such as www, api, app and docs are refused. Defaults to a name derived from the app name, with a suffix added if that name is taken. Uppercase letters are accepted and stored in lowercase. A value you send that another app already holds returns 409 APP_HANDLE_TAKEN, and one that was just released returns 409 APP_HANDLE_HELD with details.available_at.

Responses

201App created
FieldTypeDescription
idstring
org_idstring
namestring
ios_bundle_idstring | null
ios_team_idstring | null
ios_app_store_idstring | null
ios_app_store_urlstring | null
ios_universal_link_pathsarray | null
android_package_namestring | null
android_sha256_fingerprintsarray | null
android_play_store_urlstring | null
android_app_links_pathsarray | null
icon_urlstring | null
icon_source"auto" | "manual"
probabilistic_matchingbooleanFalse means routing-only mode: the server no longer matches installs by probabilistic match or raw device signals for this app. Deep links keep working, and deferred links and attribution use only deterministic signals (the Android Play Install Referrer click id, and a device that already has an install). Defaults to true.
link_handlestringThe lowercase subdomain label for this app's links, for example acme in acme.aplnk.to. Every app has one.
link_domainstringThe full host derived from link_handle, for example acme.aplnk.to.
legacy_apex_enabledbooleanTrue for an app that existed when app subdomains launched. Its links on aplnk.to keep working and aplnk.to keeps listing the app for good.
default_link_domainstringThe domain a new link gets when a create request names none: the saved default, or link_domain when none is saved. Change it with PUT /v1/apps/{id}/link-domain. It never changes a link that already exists.
default_link_domain_availablebooleanFalse when the saved default is a custom domain that is no longer live. A link created with no domain then gets 409 DEFAULT_DOMAIN_UNAVAILABLE until you choose a domain or a new default.
allowed_link_domainsstring[]The hostnames a new link for this app may use: the default first when it is available, then aplnk.to (legacy apps only), link_domain and the org's live custom domains.
previous_link_handlestring | nullThe app's old subdomain label while a rename period is open. null otherwise.
previous_link_domainstring | nullThe full old host while a rename period is open, for example oldname.aplnk.to. It keeps opening the app.
rename_period_ends_atstring | nullThe date shown for the open rename period. It is not a deadline and not a promise to move links that day. If links on the old subdomain later move to the new one, their old URLs redirect. null when no period is open.
rename_period_extensionsintegerHow many times the open rename period was extended by 1 month. At most 2.
created_atstring
updated_atstring
400Validation error
FieldTypeDescription
error*object
401Unauthorized
FieldTypeDescription
error*object
403Forbidden
FieldTypeDescription
error*object
409Duplicate bundle ID or package name, or link_handle is already taken (APP_HANDLE_TAKEN)
FieldTypeDescription
error*object

At least one of ios_bundle_id or android_package_name must be present. Everything else, including the store URLs and universal/app link paths, is optional and can be added later with an update.

List Apps

GET/apps

List apps

Parameters

NameTypeInDescription
limitintegerquery
offsetintegerquery
searchstringquery

Responses

200List of apps
FieldTypeDescription
dataobject[]
paginationobject
401Unauthorized
FieldTypeDescription
error*object
403Forbidden
FieldTypeDescription
error*object

Supports an optional search parameter (up to 200 characters) to filter by name, alongside the standard limit and offset pagination shared across list endpoints.

Get an App

GET/apps/{id}

Get an app

Parameters

NameTypeInDescription
id*stringpath

Responses

200App details
FieldTypeDescription
idstring
org_idstring
namestring
ios_bundle_idstring | null
ios_team_idstring | null
ios_app_store_idstring | null
ios_app_store_urlstring | null
ios_universal_link_pathsarray | null
android_package_namestring | null
android_sha256_fingerprintsarray | null
android_play_store_urlstring | null
android_app_links_pathsarray | null
icon_urlstring | null
icon_source"auto" | "manual"
probabilistic_matchingbooleanFalse means routing-only mode: the server no longer matches installs by probabilistic match or raw device signals for this app. Deep links keep working, and deferred links and attribution use only deterministic signals (the Android Play Install Referrer click id, and a device that already has an install). Defaults to true.
link_handlestringThe lowercase subdomain label for this app's links, for example acme in acme.aplnk.to. Every app has one.
link_domainstringThe full host derived from link_handle, for example acme.aplnk.to.
legacy_apex_enabledbooleanTrue for an app that existed when app subdomains launched. Its links on aplnk.to keep working and aplnk.to keeps listing the app for good.
default_link_domainstringThe domain a new link gets when a create request names none: the saved default, or link_domain when none is saved. Change it with PUT /v1/apps/{id}/link-domain. It never changes a link that already exists.
default_link_domain_availablebooleanFalse when the saved default is a custom domain that is no longer live. A link created with no domain then gets 409 DEFAULT_DOMAIN_UNAVAILABLE until you choose a domain or a new default.
allowed_link_domainsstring[]The hostnames a new link for this app may use: the default first when it is available, then aplnk.to (legacy apps only), link_domain and the org's live custom domains.
previous_link_handlestring | nullThe app's old subdomain label while a rename period is open. null otherwise.
previous_link_domainstring | nullThe full old host while a rename period is open, for example oldname.aplnk.to. It keeps opening the app.
rename_period_ends_atstring | nullThe date shown for the open rename period. It is not a deadline and not a promise to move links that day. If links on the old subdomain later move to the new one, their old URLs redirect. null when no period is open.
rename_period_extensionsintegerHow many times the open rename period was extended by 1 month. At most 2.
created_atstring
updated_atstring
401Unauthorized
FieldTypeDescription
error*object
403Forbidden
FieldTypeDescription
error*object
404Not found
FieldTypeDescription
error*object

Returns the full app configuration, including whichever platform fields are set. Fields for a platform you haven't configured come back null rather than omitted, so a client can always read ios_bundle_id and android_package_name without checking for their presence first.

Update an App

PATCH/apps/{id}

Update an app

Parameters

NameTypeInDescription
id*stringpath

Request Body

FieldTypeDescription
namestring
ios_bundle_idstring | null
ios_team_idstring | null
ios_app_store_idstring | null
ios_app_store_urlstring | null
ios_universal_link_pathsarray | null
android_package_namestring | null
android_sha256_fingerprintsarray | null
android_play_store_urlstring | null
android_app_links_pathsarray | null
probabilistic_matchingbooleanSet false to turn on routing-only mode for this app, true to turn probabilistic matching back on. The change applies to the match endpoint at once. Links already shared keep working either way.
link_handlestringThe subdomain label for this app: 3 to 30 characters, lowercase letters, numbers and hyphens, not starting or ending with a hyphen. Reserved names such as www, api, app, docs and test are refused. Uppercase letters are accepted and stored in lowercase. It needs an API key that has a creator, and expected_link_handle. Sending the current label confirms it. An unused subdomain (no link and no redirect uses it) changes on any plan at once, with no confirmation and no rename period, and the old name is held for 7 days: only this app can take it back before then. Once links or redirects use it, one more rename is allowed on every plan, needs confirm: true and keeps the old subdomain opening the app (rename_period_ends_at is not a deadline); if its links move to the new subdomain, their old URLs redirect. Rejected with 409 RENAME_PERIOD_OPEN while an earlier rename period is open. Checked for uniqueness against every app subdomain.
expected_link_handlestringThe link_handle the app has now, as you last read it. Required whenever link_handle is sent. A different current handle returns 409 APP_LINK_STATE_CHANGED and writes nothing.
confirmbooleanSet to true to rename while links still use the old host. Without it the request returns 409 CONFIRMATION_REQUIRED with details.links_on_old_host. Not needed for an unused subdomain.

Responses

200Updated app
FieldTypeDescription
idstring
org_idstring
namestring
ios_bundle_idstring | null
ios_team_idstring | null
ios_app_store_idstring | null
ios_app_store_urlstring | null
ios_universal_link_pathsarray | null
android_package_namestring | null
android_sha256_fingerprintsarray | null
android_play_store_urlstring | null
android_app_links_pathsarray | null
icon_urlstring | null
icon_source"auto" | "manual"
probabilistic_matchingbooleanFalse means routing-only mode: the server no longer matches installs by probabilistic match or raw device signals for this app. Deep links keep working, and deferred links and attribution use only deterministic signals (the Android Play Install Referrer click id, and a device that already has an install). Defaults to true.
link_handlestringThe lowercase subdomain label for this app's links, for example acme in acme.aplnk.to. Every app has one.
link_domainstringThe full host derived from link_handle, for example acme.aplnk.to.
legacy_apex_enabledbooleanTrue for an app that existed when app subdomains launched. Its links on aplnk.to keep working and aplnk.to keeps listing the app for good.
default_link_domainstringThe domain a new link gets when a create request names none: the saved default, or link_domain when none is saved. Change it with PUT /v1/apps/{id}/link-domain. It never changes a link that already exists.
default_link_domain_availablebooleanFalse when the saved default is a custom domain that is no longer live. A link created with no domain then gets 409 DEFAULT_DOMAIN_UNAVAILABLE until you choose a domain or a new default.
allowed_link_domainsstring[]The hostnames a new link for this app may use: the default first when it is available, then aplnk.to (legacy apps only), link_domain and the org's live custom domains.
previous_link_handlestring | nullThe app's old subdomain label while a rename period is open. null otherwise.
previous_link_domainstring | nullThe full old host while a rename period is open, for example oldname.aplnk.to. It keeps opening the app.
rename_period_ends_atstring | nullThe date shown for the open rename period. It is not a deadline and not a promise to move links that day. If links on the old subdomain later move to the new one, their old URLs redirect. null when no period is open.
rename_period_extensionsintegerHow many times the open rename period was extended by 1 month. At most 2.
created_atstring
updated_atstring
400Validation error
FieldTypeDescription
error*object
401Unauthorized
FieldTypeDescription
error*object
403Forbidden. ATTRIBUTED_KEY_REQUIRED when the API key has no creator and link_handle is sent. FORBIDDEN when the key lacks apps:write, or its creator is no longer in the organization.
FieldTypeDescription
error*object
404Not found
FieldTypeDescription
error*object
409DUPLICATE_APP for a bundle ID or package name collision. APP_HANDLE_TAKEN when link_handle is already taken. APP_HANDLE_HELD when link_handle was just released and is held for 7 days (details.available_at). RENAME_PERIOD_OPEN while an earlier rename period is open (details.previous_link_handle and details.rename_period_ends_at). CONFIRMATION_REQUIRED when links use the old host and confirm is not true; details.links_on_old_host is the link count. APP_LINK_STATE_CHANGED when expected_link_handle is not the current handle, or when a link started to use the subdomain before an unused-subdomain change was written.
FieldTypeDescription
error*object

A PATCH, not a full replace: fields you omit keep their current value, and fields you send as an explicit value overwrite it. The one exception is the platform-configured rule. If your update would leave the app with neither ios_bundle_id nor android_package_name, whether by explicitly nulling the last remaining one or by leaving it unset while the other was never set, the update is refused with 400 VALIDATION_ERROR before anything is written.

Delete an App

DELETE/apps/{id}

Delete an app

Parameters

NameTypeInDescription
id*stringpath

Responses

200Deleted app
FieldTypeDescription
idstring
org_idstring
namestring
ios_bundle_idstring | null
ios_team_idstring | null
ios_app_store_idstring | null
ios_app_store_urlstring | null
ios_universal_link_pathsarray | null
android_package_namestring | null
android_sha256_fingerprintsarray | null
android_play_store_urlstring | null
android_app_links_pathsarray | null
icon_urlstring | null
icon_source"auto" | "manual"
probabilistic_matchingbooleanFalse means routing-only mode: the server no longer matches installs by probabilistic match or raw device signals for this app. Deep links keep working, and deferred links and attribution use only deterministic signals (the Android Play Install Referrer click id, and a device that already has an install). Defaults to true.
link_handlestringThe lowercase subdomain label for this app's links, for example acme in acme.aplnk.to. Every app has one.
link_domainstringThe full host derived from link_handle, for example acme.aplnk.to.
legacy_apex_enabledbooleanTrue for an app that existed when app subdomains launched. Its links on aplnk.to keep working and aplnk.to keeps listing the app for good.
default_link_domainstringThe domain a new link gets when a create request names none: the saved default, or link_domain when none is saved. Change it with PUT /v1/apps/{id}/link-domain. It never changes a link that already exists.
default_link_domain_availablebooleanFalse when the saved default is a custom domain that is no longer live. A link created with no domain then gets 409 DEFAULT_DOMAIN_UNAVAILABLE until you choose a domain or a new default.
allowed_link_domainsstring[]The hostnames a new link for this app may use: the default first when it is available, then aplnk.to (legacy apps only), link_domain and the org's live custom domains.
previous_link_handlestring | nullThe app's old subdomain label while a rename period is open. null otherwise.
previous_link_domainstring | nullThe full old host while a rename period is open, for example oldname.aplnk.to. It keeps opening the app.
rename_period_ends_atstring | nullThe date shown for the open rename period. It is not a deadline and not a promise to move links that day. If links on the old subdomain later move to the new one, their old URLs redirect. null when no period is open.
rename_period_extensionsintegerHow many times the open rename period was extended by 1 month. At most 2.
created_atstring
updated_atstring
401Unauthorized
FieldTypeDescription
error*object
403Forbidden. FORBIDDEN when the key lacks apps:write, or its creator is a member or no longer in the organization. Deleting an app needs an owner or admin creator.
FieldTypeDescription
error*object
404Not found
FieldTypeDescription
error*object

Deletes the app and returns the deleted row. This is permanent. Do this only after confirming which links point at the app, since every link that does becomes a web-only link the moment the delete completes.

Every app has its own link subdomain, {link_handle}.aplnk.to, on every plan. An app that existed before subdomains keeps aplnk.to for good: links already on it keep working and keep opening your app, and aplnk.to stays one of the app's allowed link domains. Nothing moves on a date, and no link changes address unless you change it. An app created after the change uses its own subdomain (or a custom domain) for new links. Web-only links (links with no app, whether existing or created later in an org that has an app) default to the shared aplnk.to domain (a custom domain is also allowed). Registering an app does not add deep linking to them.

An app response carries these fields:

FieldTypeNotes
link_handlestringThe subdomain label. Writable on create, and on update as a rename
link_domainstringThe full host, read only
legacy_apex_enabledbooleanTrue for an app that existed before subdomains. It never changes. Read only
default_link_domainstringThe hostname a new link gets when the request names no domain. Change it with the link domain endpoint below
default_link_domain_availablebooleanFalse when the saved default is a custom domain that is no longer live. Read only
allowed_link_domainsstring[]The hostnames a new link may use, the default first when it is available. Read only
previous_link_handlestring or nullThe old subdomain label while a rename period is open. Read only
previous_link_domainstring or nullThe full old host while a rename period is open. Read only
rename_period_ends_atdate-time or nullThe date shown for the rename period. The old host keeps working. Read only
rename_period_extensionsinteger, 0 to 2How many times the rename period was extended. Read only

On create, leave link_handle out and we derive it from the app name, with the bundle ID or package name as the fallback. The handle is checked against every other app's subdomain.

Changing link_handle on update follows these rules:

  • Send expected_link_handle with every link_handle: the handle you last read. If the app's handle is another, the request gets 409 APP_LINK_STATE_CHANGED and nothing is written. Sending the current handle changes nothing except confirming the name once.
  • An app whose subdomain no link and no redirect uses changes it in a plain edit on every plan, as often as you like. There is no confirm and no rename period. The old name is held for 7 days: only your app can take it back before then. If a link starts using the subdomain first, the request gets 409 APP_LINK_STATE_CHANGED.
  • Once a link or redirect uses the subdomain, one more rename is allowed. It needs confirm: true and works on every plan, Free included. Links you already made stay on the old subdomain.
  • Through an API key, the key needs a creator. A key with no creator gets 403 ATTRIBUTED_KEY_REQUIRED. A key whose creator has left the organization gets 403 FORBIDDEN.
  • A rename that starts a period is refused with 409 RENAME_PERIOD_OPEN while an earlier rename period is open, including a rename back to the old name. details carries previous_link_handle and rename_period_ends_at.
  • When links use the old host, send confirm: true. Without it the request gets 409 CONFIRMATION_REQUIRED with links_on_old_host in details. After such a rename the old host keeps opening the app. If its links move to the new host, they keep their slug and their old URLs redirect.
  • The slug test is reserved on app subdomains: https://{link_domain}/test is the setup test page, so no link can use it there.
  • link_handle: null is a 400 VALIDATION_ERROR. Every app keeps a subdomain.

Through MCP, the update_app tool takes the same link_handle, expected_link_handle and confirm fields and follows the same rules. A call that needs the confirmation returns the link count and asks you to retry with confirm: true. Call it once without confirm. Tell the user that the old subdomain keeps opening the app, that if its links move to the new subdomain their old URLs redirect, and that app builds need the new subdomain. Set confirm only after they agree. The extend_subdomain_rename tool extends a rename period. The list_link_moves tool lists the links that still use the previous subdomain and set_link_move_choice sets the choice for one of them, like the endpoints below.

A link that names no domain gets the app's default_link_domain. An app that existed before subdomains starts with the domain of its most recent link, or aplnk.to when that domain cannot serve new links, and any member of the organization can change it to aplnk.to (legacy apps only), the app's own link_domain, or a live custom domain, and change it back at any time. Existing links keep their addresses and no redirect is created. Send the link_handle and default_link_domain you last read, and confirm: true. A change made meanwhile returns 409 APP_LINK_STATE_CHANGED. The app's allowed_link_domains lists the valid choices.

When the default is a custom domain that is no longer live, default_link_domain_available is false and POST /v1/links with no domain returns 409 DEFAULT_DOMAIN_UNAVAILABLE with details.allowed_link_domains. Name an allowed domain, or choose a new default. An explicit domain is never replaced.

Through MCP, the set_app_link_domain tool does the same and needs the apps:write scope.

PUT/apps/{id}/link-domain

Set an app's default link domain

Chooses the default domain for the app's new links: aplnk.to (legacy apps only), the app's own subdomain, or a live custom domain. Reversible. Existing links keep their addresses, and no redirect is created. Needs an API key that has a creator. Send the link_handle and default_link_domain you last read, so a change made meanwhile is never overwritten. A request already at the requested default returns the current app without a change.

Parameters

NameTypeInDescription
id*stringpath

Request Body

FieldTypeDescription
domain*stringThe hostname new links use by default. One of allowed_link_domains on the app.
expected_link_handle*stringThe app's link_handle as you last read it.
expected_default_link_domain*stringThe app's default_link_domain as you last read it.
confirmbooleanMust be true for a change. Without it a change is refused with 400 VALIDATION_ERROR.

Responses

200The app with its new default
FieldTypeDescription
idstring
org_idstring
namestring
ios_bundle_idstring | null
ios_team_idstring | null
ios_app_store_idstring | null
ios_app_store_urlstring | null
ios_universal_link_pathsarray | null
android_package_namestring | null
android_sha256_fingerprintsarray | null
android_play_store_urlstring | null
android_app_links_pathsarray | null
icon_urlstring | null
icon_source"auto" | "manual"
probabilistic_matchingbooleanFalse means routing-only mode: the server no longer matches installs by probabilistic match or raw device signals for this app. Deep links keep working, and deferred links and attribution use only deterministic signals (the Android Play Install Referrer click id, and a device that already has an install). Defaults to true.
link_handlestringThe lowercase subdomain label for this app's links, for example acme in acme.aplnk.to. Every app has one.
link_domainstringThe full host derived from link_handle, for example acme.aplnk.to.
legacy_apex_enabledbooleanTrue for an app that existed when app subdomains launched. Its links on aplnk.to keep working and aplnk.to keeps listing the app for good.
default_link_domainstringThe domain a new link gets when a create request names none: the saved default, or link_domain when none is saved. Change it with PUT /v1/apps/{id}/link-domain. It never changes a link that already exists.
default_link_domain_availablebooleanFalse when the saved default is a custom domain that is no longer live. A link created with no domain then gets 409 DEFAULT_DOMAIN_UNAVAILABLE until you choose a domain or a new default.
allowed_link_domainsstring[]The hostnames a new link for this app may use: the default first when it is available, then aplnk.to (legacy apps only), link_domain and the org's live custom domains.
previous_link_handlestring | nullThe app's old subdomain label while a rename period is open. null otherwise.
previous_link_domainstring | nullThe full old host while a rename period is open, for example oldname.aplnk.to. It keeps opening the app.
rename_period_ends_atstring | nullThe date shown for the open rename period. It is not a deadline and not a promise to move links that day. If links on the old subdomain later move to the new one, their old URLs redirect. null when no period is open.
rename_period_extensionsintegerHow many times the open rename period was extended by 1 month. At most 2.
created_atstring
updated_atstring
400VALIDATION_ERROR for an invalid app ID or body, a domain the app cannot use (fields.domain), or a change without confirm (fields.confirm).
FieldTypeDescription
error*object
401Unauthorized
FieldTypeDescription
error*object
403Forbidden. ATTRIBUTED_KEY_REQUIRED for a key with no creator.
FieldTypeDescription
error*object
404Not found
FieldTypeDescription
error*object
409APP_LINK_STATE_CHANGED when the link_handle or default_link_domain you sent is no longer current. Read the app and review the settings before you retry.
FieldTypeDescription
error*object

Extend a Rename

After a rename the old subdomain keeps working. The rename period has an end date. Each call adds 1 month to it, up to 2 times, and only while the period is open.

POST/apps/{id}/extend-subdomain-rename

Extend an app's rename period

Adds 1 month to the rename period's end date. The previous subdomain keeps working. Up to 2 times, and only before the period ends. Takes no body.

Parameters

NameTypeInDescription
id*stringpath

Responses

200The app with its new rename period end
FieldTypeDescription
idstring
org_idstring
namestring
ios_bundle_idstring | null
ios_team_idstring | null
ios_app_store_idstring | null
ios_app_store_urlstring | null
ios_universal_link_pathsarray | null
android_package_namestring | null
android_sha256_fingerprintsarray | null
android_play_store_urlstring | null
android_app_links_pathsarray | null
icon_urlstring | null
icon_source"auto" | "manual"
probabilistic_matchingbooleanFalse means routing-only mode: the server no longer matches installs by probabilistic match or raw device signals for this app. Deep links keep working, and deferred links and attribution use only deterministic signals (the Android Play Install Referrer click id, and a device that already has an install). Defaults to true.
link_handlestringThe lowercase subdomain label for this app's links, for example acme in acme.aplnk.to. Every app has one.
link_domainstringThe full host derived from link_handle, for example acme.aplnk.to.
legacy_apex_enabledbooleanTrue for an app that existed when app subdomains launched. Its links on aplnk.to keep working and aplnk.to keeps listing the app for good.
default_link_domainstringThe domain a new link gets when a create request names none: the saved default, or link_domain when none is saved. Change it with PUT /v1/apps/{id}/link-domain. It never changes a link that already exists.
default_link_domain_availablebooleanFalse when the saved default is a custom domain that is no longer live. A link created with no domain then gets 409 DEFAULT_DOMAIN_UNAVAILABLE until you choose a domain or a new default.
allowed_link_domainsstring[]The hostnames a new link for this app may use: the default first when it is available, then aplnk.to (legacy apps only), link_domain and the org's live custom domains.
previous_link_handlestring | nullThe app's old subdomain label while a rename period is open. null otherwise.
previous_link_domainstring | nullThe full old host while a rename period is open, for example oldname.aplnk.to. It keeps opening the app.
rename_period_ends_atstring | nullThe date shown for the open rename period. It is not a deadline and not a promise to move links that day. If links on the old subdomain later move to the new one, their old URLs redirect. null when no period is open.
rename_period_extensionsintegerHow many times the open rename period was extended by 1 month. At most 2.
created_atstring
updated_atstring
400Invalid app ID
FieldTypeDescription
error*object
401Unauthorized
FieldTypeDescription
error*object
403Forbidden
FieldTypeDescription
error*object
404Not found
FieldTypeDescription
error*object
409RENAME_PERIOD_NOT_OPEN when the app has no open rename period, or it already ended. RENAME_EXTENSION_LIMIT when both extensions were used (details.limit is 2).
FieldTypeDescription
error*object

After a rename, the links still on the old subdomain can move to the new one, and each old URL then redirects with a permanent 301. The old address keeps working, and rename_period_ends_at is the date shown for the rename period. Links on aplnk.to never move.

During a rename period, a new link on the app's subdomain cannot take a slug that one of the app's old-host links still has. POST /v1/links returns 409 SLUG_TAKEN with the message "You already have yourapp.aplnk.to/abc, so this address is held for it. Pick another slug." and details.held_by with the old domain and slug. The dashboard and the MCP create_link tool refuse the same slug. A link whose slug was already taken on the target is a conflict. It stays where it is unless you choose a new slug for it, and you can also mark it to keep. Set the choice with PUT /v1/links/{id}/move-choice (see Links).

GET/apps/{id}/link-moves

List an app's link moves

Shows the links that still use an app's previous subdomain, and which cannot keep their slug on the app's current subdomain because it is taken there. Conflicts come first. If a link later moves, its old URL answers a permanent redirect. An app with no previous subdomain in use returns an empty moves list. Needs the links:read scope.

Parameters

NameTypeInDescription
id*stringpath
limitintegerqueryLinks per move.
offsetintegerqueryLinks to skip in each move.

Responses

200The app's pending moves
FieldTypeDescription
movesobject[]
400Invalid app ID or page
FieldTypeDescription
error*object
401Unauthorized
FieldTypeDescription
error*object
403Forbidden
FieldTypeDescription
error*object
404Not found
FieldTypeDescription
error*object

status is scheduled (a move is planned, with moves_at set when we have a date) or moving (the move is running).

Worked Example

Register an app with both platforms, add an Android app link path later, then remove it once it's decommissioned:

Common Errors

StatusCodeCause
400VALIDATION_ERRORA field fails its format check, or neither platform is configured after the change
400INVALID_IDThe id path parameter isn't a valid UUID
400IMMUTABLE_FIELDThe update body includes org_id
403FORBIDDENThe key lacks apps:read/apps:write, or its creator's role lacks the matching permission
403APP_LIMIT_EXCEEDEDThe organization's plan app limit is already reached
403ATTRIBUTED_KEY_REQUIREDA rename through an API key with no creator
404NOT_FOUNDNo app with that ID exists in this organization
409DUPLICATE_APPThe ios_bundle_id or android_package_name already belongs to another app in this org
409APP_HANDLE_TAKENThe link_handle already belongs to another app
409APP_HANDLE_HELDThe link_handle was just released and is held for 7 days. details.available_at is the instant it becomes available
409RENAME_PERIOD_OPENA rename period is already open for this app
409RENAME_PERIOD_NOT_OPENA rename extension was requested with no open period
409RENAME_EXTENSION_LIMITThe app used both rename extensions
409CONFIRMATION_REQUIREDLinks use the old host and confirm is not true

FAQ

Can an app support both iOS and Android? Yes, and it's the common case. Set ios_bundle_id and/or android_package_name on the same app row; a React Native or Flutter app typically registers both.

What happens to links when their app is deleted? They aren't deleted. Their app_id clears to null, and the link keeps working as a web-only link, with its click history intact. It just loses deep linking until it's pointed at a different app.

Do I need to publish apple-app-site-association or assetlinks.json myself? No. Both files regenerate automatically whenever an app's identifiers, fingerprints, or link paths change, and they're served from your domain without a separate deploy step.

Why is icon_url still null right after creating an app? Icon resolution runs asynchronously after the create response is sent. It typically completes within moments if the App Store ID or Play package name resolves to a public listing; poll the app with GET /v1/apps/{id} if you need to confirm it landed.

How do I turn off probabilistic matching for an app? Send PATCH /v1/apps/{id} with { "probabilistic_matching": false }, or use the Install matching switch in the app's settings. Links keep redirecting and opening your app. Installs are then matched only by deterministic signals. See Routing-only mode.

Can a member role update app configuration? Yes. Every role can read, create, and update apps, including the link address and the default link domain. Deleting an app requires the apps:delete permission, which only owner and admin roles hold.