Skip to content

Backup and import endpoints ​

These REST routes export and restore backups and import data from other link plugins. Each one is a job: you start it, call a progress route until it finishes, and can cancel it.

Sign-in, the response envelope and errors are in the REST API overview.

Every route here needs an administrator (manage_options, Permissions). All paths are relative to /wp-json/link-manager-pro/v1. The plugin's own screens call these routes, so build on them with care.

A backup file is JSON. Backup file format describes what it holds. The same jobs run from the command line: WP-CLI for developers.

Export a backup ​

An export is a job that runs in batches. Call POST /settings/backup/export/start with the items you want. Then call POST /settings/backup/export/progress with the returned job_id. Each call writes one batch of up to 500 rows. Repeat until status is done. The last answer holds a download_url, and you fetch the file from it.

The job payload that the start and progress routes return:

FieldTypeWhat it holds
job_idstringThe job ID (a UUID). Send it to the progress route.
typestringexport for a backup, import for a restore.
statusstringrunning, done, or failed. A restore can also be cancelled.
totalintegerRows to write, summed over all items.
processedintegerRows written so far.
percentintegerprocessed as a percentage of total, 0 to 100. A job with no rows reads 100 once done.
current_itemstring or nullThe item being written now. null when none is left.
itemsarray of stringsThe items in the job, in backup order.
items_totalobjectRows per item, keyed by item.
items_processedobjectRows written per item, keyed by item.
errorstring or nullThe failure message, or null.
download_urlstringOnly when status is done. See Download the finished file.
file_namestringOnly when status is done. The file name, link-manager-pro-backup-YYYYMMDD-HHMMSS.json (UTC).

The item keys are categories, tags, links, link_rules, clicks, affiliates, campaigns, link_health, redirects, settings, webhooks and posts. The plugin lists a job's items in this order whatever order you send. Choosing links adds the item link_meta to the job.

Start an export ​

POST /settings/backup/export/start

Creates an export job for the items you choose. It writes nothing yet.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
itemsarray of stringsYesnoneThe items to export. Keys that are not in the list above are dropped. A value that is not an array counts as empty.

Response: 200. data is the job payload with type set to export and status set to running.

bash
curl -X POST \
  -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{"items":["links","categories","tags"]}' \
  "https://example.com/wp-json/link-manager-pro/v1/settings/backup/export/start"
json
{
  "success": true,
  "data": {
    "job_id": "5f1c2a9e-3b7d-4e2a-9c1f-0a8d6b4e7f21",
    "type": "export",
    "status": "running",
    "total": 1234,
    "processed": 0,
    "percent": 0,
    "current_item": "categories",
    "items": ["categories", "tags", "links", "link_meta"],
    "items_total": { "categories": 4, "tags": 7, "links": 1200, "link_meta": 23 },
    "items_processed": { "categories": 0, "tags": 0, "links": 0, "link_meta": 0 },
    "error": null
  }
}

Errors:

StatusWhen
400None of the items you sent is a valid item. The code is backup_items_required.
409A restore is unfinished. Finish or cancel it first. The code is backup_import_running.

Run an export batch ​

POST /settings/backup/export/progress

Writes the next batch of the job and returns the new progress. Call it until status is done.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
job_idstringYesnoneThe job_id from the start route.

Response: 200. data is the job payload. A job that is already done returns its payload again with a fresh download_url.

json
{
  "success": true,
  "data": {
    "job_id": "5f1c2a9e-3b7d-4e2a-9c1f-0a8d6b4e7f21",
    "type": "export",
    "status": "done",
    "total": 1234,
    "processed": 1234,
    "percent": 100,
    "current_item": null,
    "items": ["categories", "tags", "links", "link_meta"],
    "items_total": { "categories": 4, "tags": 7, "links": 1200, "link_meta": 23 },
    "items_processed": { "categories": 4, "tags": 7, "links": 1200, "link_meta": 23 },
    "error": null,
    "download_url": "https://example.com/wp-admin/admin-ajax.php?action=link_manager_pro_download_backup&job_id=5f1c2a9e-3b7d-4e2a-9c1f-0a8d6b4e7f21&_wpnonce=1a2b3c4d5e",
    "file_name": "link-manager-pro-backup-20261023-101500.json"
  }
}

Errors:

StatusWhen
400job_id is empty. The code is backup_job_required.
404No job has that ID. The code is backup_job_not_found.
500The job failed. The message says why, for example backup_export_fetch_failed (rows could not be read) or backup_job_failed.
507The server ran out of disk space while writing. The code is backup_disk_full.

Download the finished file ​

GET /wp-admin/admin-ajax.php?action=link_manager_pro_download_backup&job_id={job_id}&_wpnonce={nonce}

Sends the finished backup file. This is an admin-ajax request, not a REST route, so it needs a logged-in browser session (the login cookie). Use the download_url from the done payload as it is.

Permission: manage_options.

Query parameters:

NameTypeRequiredDefaultWhat it does
actionstringYesnoneAlways link_manager_pro_download_backup.
job_idstringYesnoneThe export job ID.
_wpnoncestringYesnoneA nonce for the action lmp_backup_download_{job_id}. The download_url already carries it.

Response: 200 with the file as the body. The headers are Content-Type: application/json, Content-Disposition: attachment; filename="link-manager-pro-backup-....json", Content-Length and Cache-Control: no-cache, no-store, must-revalidate. The body is the backup file (Backup file format).

Errors:

StatusWhen
403The user is not an administrator, or the nonce is missing, wrong or expired.
404The job does not exist, is not a finished export, or its file is gone.

The server keeps the file for one day after the job's last activity. After that the daily clean-up deletes it, and you export again.

GET /settings/backup/export/csv

Streams every short link as a CSV file. This is a spreadsheet list, not a restorable backup.

Permission: manage_options (Permissions)

Query parameters: None.

Response: 200 with a CSV body, not the JSON envelope. The headers are Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment; filename="link-manager-pro-links-YYYY-MM-DD.csv" (the UTC date) and Cache-Control: no-cache, no-store, must-revalidate. Rows run from the highest link ID down. A cell that starts with =, +, -, @, a tab or a carriage return gets a leading ' so spreadsheets do not run it as a formula.

ColumnWhat it holds
IDThe short link ID.
SlugThe slug.
Target URLThe destination.
Redirect TypeThe HTTP status, 301 when the link has none.
SourceWhere the link came from, such as manual.
Click CountThe stored click total, 0 when none.
Autolinkyes or no.
KeywordsThe auto-link keywords joined with a comma and a space.
Created AtThe creation date and time, written the way the link model prints it. Empty when none.
bash
curl -u USER:APPLICATION_PASSWORD -o links.csv \
  "https://example.com/wp-json/link-manager-pro/v1/settings/backup/export/csv"
text
ID,Slug,"Target URL","Redirect Type",Source,"Click Count",Autolink,Keywords,"Created At"
42,blue-widget,https://example.com/products/blue-widget,301,manual,128,yes,"blue widget, widget",2026-10-01T09:30:00.000

Restore a backup ​

A restore is also a job. You send the file and the items to restore, then call the progress route until it ends. The plugin keeps a safety copy of each item before it clears it. If a batch fails, or you cancel, it puts the copies back. Only one restore can run at a time, and a restore blocks new exports.

A small file goes to POST /settings/backup/import/start as the field file. A larger file goes up in parts to POST /settings/backup/import/chunk first. The last part returns an upload_id. You send that to the start route instead of the file.

The flow:

  1. Send the file, or upload its parts and keep the upload_id.
  2. POST /settings/backup/import/start with items and file or upload_id. The plugin checks the file here. A refused file never touches your data.
  3. POST /settings/backup/import/progress until status is done. Each call restores one batch of up to 500 rows (10 for posts).
  4. To stop, call POST /settings/backup/import/cancel. A failed batch rolls back on its own and returns 500.

If a page closes mid-restore, GET /settings/backup/import/active returns the running job so you can resume it.

The restore job uses the same fields as the export job payload, with type set to import. A done restore adds these:

FieldTypeWhat it holds
messagestringImport completed successfully.
notesstringOnly when something was skipped. A plain sentence, for example about posts that were not restored or a short link prefix that was left out.

A progress call can also return busy set to true. That means another request is restoring a batch right now. Wait and call again.

Restore rules you can hit as errors:

  • The file must be a .json file with JSON content and a schema_version the plugin knows.
  • posts can only be restored on the site the backup came from.
  • A backup from another site must include the items that depend on each other. Restoring links without link_rules, clicks, redirects, categories and tags is refused when this site has rows in them.

Upload one part of a backup ​

POST /settings/backup/import/chunk

Adds one part of a large backup file. Send the parts in order, as multipart/form-data.

Permission: manage_options (Permissions)

Body parameters: (multipart form, not JSON)

NameTypeRequiredDefaultWhat it does
upload_idstringNoemptyEmpty on the first part. On later parts, the upload_id the previous answer returned.
indexintegerYes0The part number, counting from 0. It must equal the next_index of the last answer.
totalintegerYes0How many parts the upload has. Must be 1 or more, and above index.
sizeintegerYes0The whole file's size in bytes. Must be above 0 and at most 1073741824 (1 GB). Read on the first part.
namestringYesemptyThe file name. It must end in .json. Read on the first part.
partfileYesnoneThe bytes of this part. It can be no larger than the server's upload limit (wp_max_upload_size()).

The plugin's own screen sends parts of up to 1 MB, or the server limit minus 64 KB when that is smaller. It halves a part the server refuses, down to 128 KB.

Response: 200. data holds:

FieldTypeWhat it holds
upload_idstringThe upload ID (a UUID). Send it with every later part and with the start route.
next_indexintegerThe index the next part must carry.
bytesintegerBytes stored so far.
sizeintegerThe whole file's size in bytes.
completebooleantrue when bytes equals size.
bash
curl -X POST \
  -u USER:APPLICATION_PASSWORD \
  -F "upload_id=" -F "index=0" -F "total=3" -F "size=2621440" -F "name=backup.json" \
  -F "[email protected]" \
  "https://example.com/wp-json/link-manager-pro/v1/settings/backup/import/chunk"
json
{
  "success": true,
  "data": {
    "upload_id": "9b2e41c0-7a3d-4f6e-8d15-3c0b7a9e2f64",
    "next_index": 1,
    "bytes": 1048576,
    "size": 2621440,
    "complete": false
  }
}

Errors:

StatusWhen
400The part is missing or empty (backup_upload_part_missing), the transfer failed (backup_file_upload_failed), the name does not end in .json (backup_file_extension), or the sizes do not add up (backup_upload_invalid).
403Another user started this upload. The code is backup_upload_forbidden.
404The upload_id is unknown or expired, or index is not 0 on a first part. The code is backup_upload_not_found.
409The part arrived out of order. The code is backup_upload_part_order. The error data holds expected_index and bytes so you can resume from there.
413The part is larger than the server takes (backup_upload_part_too_large, with limit in the error data), or the file is over 1 GB (backup_upload_too_large).
500The part could not be stored. The code is backup_file_store_failed.

Start a restore ​

POST /settings/backup/import/start

Checks the backup file and creates the restore job. It clears nothing yet; each item is cleared when its first batch runs.

Permission: manage_options (Permissions)

Body parameters: (multipart form when you send file, JSON otherwise)

NameTypeRequiredDefaultWhat it does
itemsarray of stringsYesnoneThe items to restore, from the item list. Send them as items[] in a form. Unknown keys are dropped. The file must hold every item you choose.
filefileOne of file or upload_idnoneThe backup file, sent in one request.
upload_idstringOne of file or upload_idemptyThe ID of a complete chunked upload made by the same user. When set, file is ignored. The plugin deletes the upload after it takes the file.

Response: 200. data is the job payload with type set to import and status set to running.

bash
curl -X POST \
  -u USER:APPLICATION_PASSWORD \
  -F "[email protected]" \
  -F "items[]=links" -F "items[]=categories" -F "items[]=tags" \
  "https://example.com/wp-json/link-manager-pro/v1/settings/backup/import/start"
json
{
  "success": true,
  "data": {
    "job_id": "c3d8f0a2-61b4-4d97-a2e5-7f9014be3a18",
    "type": "import",
    "status": "running",
    "total": 1234,
    "processed": 0,
    "percent": 0,
    "current_item": "categories",
    "items": ["categories", "tags", "links", "link_meta"],
    "items_total": { "categories": 4, "tags": 7, "links": 1200, "link_meta": 23 },
    "items_processed": { "categories": 0, "tags": 0, "links": 0, "link_meta": 0 },
    "error": null
  }
}

Errors:

StatusWhen
400No valid item (backup_items_required). No file (backup_file_required) or a failed upload (backup_file_upload_failed, backup_file_upload_invalid). Not a .json name (backup_file_extension) or not JSON content (backup_file_mime). A bad schema_version (backup_file_invalid) or one newer than the plugin knows (backup_newer_format). A chosen item is missing or malformed in the file (backup_item_missing, backup_item_invalid). Posts from another site (backup_posts_foreign_site). Missing dependent items (backup_cross_site_dependants, with an items list in the error data).
404The upload_id is unknown or expired (backup_upload_not_found).
403The upload_id belongs to another user (backup_upload_forbidden).
409A restore is already running (backup_import_running), or the upload is not complete (backup_upload_incomplete).
500The file could not be stored (backup_file_store_failed).
507The server has no disk space to prepare the files (backup_disk_full).

Run a restore batch ​

POST /settings/backup/import/progress

Restores the next batch of rows and returns the new progress. Call it until status is done.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
job_idstringYesnoneThe job_id from the start route.

Response: 200. data is the job payload, plus busy: true when another request holds the restore. A job that is done or cancelled returns its payload again. A done job also holds message and, when something was skipped, notes.

json
{
  "success": true,
  "data": {
    "job_id": "c3d8f0a2-61b4-4d97-a2e5-7f9014be3a18",
    "type": "import",
    "status": "done",
    "total": 1234,
    "processed": 1234,
    "percent": 100,
    "current_item": null,
    "items": ["categories", "tags", "links", "link_meta"],
    "items_total": { "categories": 4, "tags": 7, "links": 1200, "link_meta": 23 },
    "items_processed": { "categories": 4, "tags": 7, "links": 1200, "link_meta": 23 },
    "error": null,
    "message": "Import completed successfully."
  }
}

Errors:

StatusWhen
400job_id is empty. The code is backup_job_required.
404No job has that ID. The code is backup_job_not_found.
500The restore failed and was rolled back (backup_import_failed). The message reads "Restore failed while restoring {item}: {cause} All data was rolled back; nothing was changed." The item is named as a readable label, such as "link rules", "link details", "Link Health records" or "posts and pages". When the run was past the last item, it names the last one, the job is already failed (backup_job_failed), or a count did not match (backup_restore_incomplete). Your previous data was put back. If the rollback itself could not finish, the message names the safety-copy tables the plugin kept.

Get the running restore ​

GET /settings/backup/import/active

Returns the unfinished restore, so a screen that reloads can resume it.

Permission: manage_options (Permissions)

Query parameters: None.

Response: 200. data is the job payload of the running restore, or null when none is running.

json
{
  "success": true,
  "data": null
}

Cancel a restore ​

POST /settings/backup/import/cancel

Stops a running restore and puts back every item it already cleared.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
job_idstringYesnoneThe restore job ID.

Response: 200. data is the job payload with status set to cancelled. error holds the plain sentence Restore cancelled. All data was rolled back; nothing was changed.

Errors:

StatusWhen
400job_id is empty. The code is backup_job_required.
404No job has that ID. The code is backup_job_not_found.
409The job is not running (backup_import_not_running), or another request is restoring a batch right now (backup_import_busy). Try again in a moment.
500The rollback could not fully finish. The message names the safety-copy tables the plugin kept.

Import from another plugin ​

Importing from Pretty Links, BetterLinks or ThirstyAffiliates is a job too. The source value is prettylinks, betterlinks or thirstyaff. The flow:

  1. GET /settings/migration/detect lists the plugins whose data it found, with counts.
  2. POST /settings/migration/start creates the job from a source, the items to bring over, a conflict_strategy and the affiliate domains to create.
  3. POST /settings/migration/process with the job_id, repeatedly. Each call runs one batch of one phase. Stop when status is done.
  4. GET /settings/migration/status reads the job without running a batch.
  5. POST /settings/migration/cancel stops the job. Rows already imported stay.

Jobs run only through these routes or WP-CLI. There is no background mode.

The job payload that start, status, process and cancel return:

FieldTypeWhat it holds
job_idstringThe job ID (a UUID).
statusstringrunning, done, failed or cancelled.
current_phasestring or nullThe phase running now, or null when all are done.
phasesarray of stringsThe phases of this job, in order. They come from the items you chose. The last one is always finalize.
phase_indexintegerThe position of the current phase in phases, from 0.
totalintegerRows to import, summed over all items.
processedintegerRows migrated, skipped or failed so far.
percentintegerprocessed as a percentage of total, 0 to 100.
statsobjectPer item: total, migrated, skipped and failed, all integers.
errorsarrayObjects with item and message.
warningsarrayObjects with item and message: what was imported differently or could not be carried over. Up to 200 are kept.
more_warningsintegerWarnings dropped after the 200.
link_checkobject or nullThe result of the last step, which resolves every imported link. null until then. Described below.

link_check holds checked, working, protected, switched_off and scheduled (all integers), problems (objects with link_id, slug and message, up to 100) and clashes (objects with link_id, slug, path, key, code, goes_to and fixable, up to 100).

The phases by source:

SourceItems you can choosePhases
prettylinkslinks, groups, categories, tags, clicks, settings, affiliatesgroups, links, terms, clicks, settings, finalize
betterlinkslinks, categories, tags, clicks, passwords, keywords, geo_rules, settings, affiliatesterms, links, keywords, passwords, geo_rules, clicks, settings, finalize
thirstyafflinks, categories, clicks, keywords, rules, settings, affiliatesterms, links, keywords, rules, clicks, settings, finalize

A phase appears only when you chose an item for it. affiliates has no phase: finalize creates the affiliates for the domains you send.

Detect other plugins ​

GET /settings/migration/detect

Lists the plugins this site has data from. A plugin with no data is left out.

Permission: manage_options (Permissions)

Query parameters: None.

Response: 200. data.plugins is an array. Each entry holds:

FieldTypeWhat it holds
detectedbooleanAlways true in this list.
sourcestringprettylinks, betterlinks or thirstyaff.
namestringThe plugin name: Pretty Links, BetterLinks or ThirstyAffiliates.
versionstringThe installed version, or an empty string when it cannot be read.
countsobjectRows per item, as integers. Includes affiliates, the number of suggested new affiliate domains.
itemsarray of stringsThe items you can import from this source (the table above).
affiliate_domainsarrayDomains of the destinations, each with domain, links, clicks, signal (why it looks like an affiliate, or an empty string), suggested and existing (both boolean). Suggested domains come first.
link_prefixstringthirstyaff only. The short-link prefix ThirstyAffiliates used.
affiliate_prefix_suggestionstringthirstyaff only. An affiliate prefix to offer, or an empty string. It is empty when an affiliate prefix is already set or ThirstyAffiliates' prefix breaks a prefix rule.
json
{
  "success": true,
  "data": {
    "plugins": [
      {
        "detected": true,
        "source": "prettylinks",
        "name": "Pretty Links",
        "version": "3.6.0",
        "counts": { "links": 120, "clicks": 4800, "groups": 3, "categories": 2, "tags": 5, "affiliates": 4 },
        "items": ["links", "groups", "categories", "tags", "clicks", "settings", "affiliates"],
        "affiliate_domains": [
          { "domain": "shop.example.net", "links": 12, "clicks": 640, "signal": "sponsored", "suggested": true, "existing": false }
        ]
      }
    ]
  }
}

Dismiss the import prompt ​

POST /settings/migration/dismiss

Remembers that you do not want the import offer for one source.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
sourcestringYesnoneThe source to stop offering, for example prettylinks. The plugin does not check the value against the three sources.

Response: 200. data is null.

Errors:

StatusWhen
400source is empty. The code is migration_source_required.

Start an import job ​

POST /settings/migration/start

Creates the import job. It brings nothing over until you call process.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
sourcestringYesnoneprettylinks, betterlinks or thirstyaff.
itemsarray of stringsYesnoneThe items to import, from the source's list above. Other values are dropped.
conflict_strategystringNoskipA source link with the same slug and destination as a link here is the same link. skip leaves that link as it is and only adds the source's clicks and old address to it. overwrite replaces its settings but never its click history or its auto-linking settings. A link with the same slug and a different destination is imported as a new link. Another value counts as skip.
affiliate_domainsarray of stringsNo[]The domains to create as affiliates. Characters other than letters, digits, dots and hyphens are removed, and the rest is lowercased. Used only when items includes affiliates.

Response: 200. data is the import job payload with status set to running.

bash
curl -X POST \
  -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{"source":"prettylinks","items":["links","categories","clicks","affiliates"],"conflict_strategy":"skip","affiliate_domains":["shop.example.net"]}' \
  "https://example.com/wp-json/link-manager-pro/v1/settings/migration/start"
json
{
  "success": true,
  "data": {
    "job_id": "e7a1b3c5-2d4f-4a68-b9c0-1f2e3d4c5b6a",
    "status": "running",
    "current_phase": "links",
    "phases": ["links", "terms", "clicks", "finalize"],
    "phase_index": 0,
    "total": 4920,
    "processed": 0,
    "percent": 0,
    "stats": {
      "links": { "total": 120, "migrated": 0, "skipped": 0, "failed": 0 },
      "categories": { "total": 2, "migrated": 0, "skipped": 0, "failed": 0 },
      "clicks": { "total": 4800, "migrated": 0, "skipped": 0, "failed": 0 },
      "affiliates": { "total": 1, "migrated": 0, "skipped": 0, "failed": 0 }
    },
    "errors": [],
    "warnings": [],
    "more_warnings": 0,
    "link_check": null
  }
}

Errors:

StatusWhen
400source is not one of the three (migration_source_invalid), items is empty or not an array (migration_items_required), or no item is valid for the source (migration_start_failed).

Run an import batch ​

POST /settings/migration/process

Runs the next batch of the job and returns the new progress. Call it until status is done.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
job_idstringYesnoneThe job_id from the start route.

Response: 200. data is the import job payload. Two extras can appear:

FieldTypeWhat it holds
busybooleantrue when another request is running a batch of this job. The payload is the current state. Call again.
affiliate_prefix_offerstringOnly when status is done. A ThirstyAffiliates import with no affiliate prefix set returns that plugin's prefix here, when it passes the prefix rules. Otherwise an empty string. The prefix is the Affiliate link prefix (General settings).

A finished job also stops the import prompt for its source.

Errors:

StatusWhen
400job_id is empty (migration_job_required), or the job is not running, for example cancelled or failed (migration_process_failed).
404No job has that ID. The code is migration_job_not_found.

A batch that throws an error sets status to failed and puts the message in errors. The route still answers 200.

Get an import job ​

GET /settings/migration/status

Reads the job's progress without running a batch.

Permission: manage_options (Permissions)

Query parameters:

NameTypeRequiredDefaultWhat it does
job_idstringYesnoneThe import job ID.

Response: 200. data is the import job payload plus affiliate_prefix_offer (a string, empty unless the job is done and the offer rules above apply).

Errors:

StatusWhen
400job_id is empty. The code is migration_job_required.
404No job has that ID. The code is migration_job_not_found.

Cancel an import job ​

POST /settings/migration/cancel

Stops the job. Rows it already imported stay in place.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
job_idstringYesnoneThe import job ID.

Response: 200. data is the import job payload with status set to cancelled.

Errors:

StatusWhen
400job_id is empty (migration_job_required), or the job could not be stopped (migration_cancel_failed).
404No job has that ID. The code is migration_job_not_found.

Replace old shortcodes ​

After an import, posts can still hold the old plugin's shortcodes: [prettylink], [thirstylink] and the <ta> tag. These three routes find them and swap each one for a normal link to the imported short link. The flow is summary, then scan, then replace. Scan and replace work through the posts in pages by post ID. Pass the next value of one answer as after in the next call, until next is null.

A shortcode whose link was not imported is left as it is and listed as unresolved. Shortcodes stored in page-builder data are only counted, and you replace them by hand.

Count the old shortcodes ​

GET /settings/migration/shortcodes

Counts the posts that still hold an imported plugin's shortcodes.

Permission: manage_options (Permissions)

Query parameters: None.

Response: 200. data holds:

FieldTypeWhat it holds
postsintegerPosts with a shortcode in their content.
builder_postsintegerPosts with a shortcode in page-builder data.
importedbooleantrue when any short link was imported from one of the three plugins.

The two counts are cached for one hour.

json
{
  "success": true,
  "data": { "posts": 14, "builder_posts": 2, "imported": true }
}

Review the shortcode changes ​

GET /settings/migration/shortcodes/scan

Lists the next 100 posts that would change, and what would change in each. It saves nothing.

Permission: manage_options (Permissions)

Query parameters:

NameTypeRequiredDefaultWhat it does
afterintegerNo0Scan posts with an ID above this. Use the next of the last answer.

Response: 200. data holds:

FieldTypeWhat it holds
postsarrayOne object per post that would change or holds an unresolved shortcode. Fields below.
nextinteger or nullThe post ID to pass as after, or null at the end.
builderobjectOnly when after is 0. Posts with shortcodes in page-builder data: posts (up to 20, each with the post fields id, title, type, status, edit_url, view_url), more (boolean) and total (integer).

Each entry in posts holds the post fields id (integer), title, type (the post type's name), status (the status label), edit_url and view_url (strings), and:

FieldTypeWhat it holds
replaceableintegerShortcodes in this post that would be replaced.
blockedstringA reason a save would be skipped (no permission, markup WordPress would strip, a scheduled post that is due). An empty string when it can be saved.
unresolvedarray of stringsUp to 10 shortcodes whose link was not imported, cut to 160 characters.
exampleobject or nullbefore and after, the first replacement found, each cut to 160 characters.
sourcesarray of stringsThe sources found, prettylinks or thirstyaff.

Replace the shortcodes ​

POST /settings/migration/shortcodes/replace

Replaces the shortcodes in the next 25 posts and saves them. Each post gets a revision first.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
afterintegerNo0Process posts with an ID above this. Use the next of the last answer.

Response: 200. data holds:

FieldTypeWhat it holds
replacedintegerShortcodes replaced in saved posts.
posts_changedintegerPosts saved.
unresolvedintegerShortcodes left because their link was not imported.
skippedarrayPosts that were not saved. Each holds the post fields (id, title, type, status, edit_url, view_url), reason (a sentence) and shortcodes (integer).
nextinteger or nullThe post ID to pass as after, or null at the end.
json
{
  "success": true,
  "data": {
    "replaced": 31,
    "posts_changed": 12,
    "unresolved": 0,
    "skipped": [],
    "next": null
  }
}

Errors:

StatusWhen
409A replacement is already running in another window. The code is shortcode_replace_busy. The lock lasts up to one minute.