partner

POST /v1/partner/customers/{externalId}/site-moves

Move one of your customer's sites onto their money-site hosting, from an archive.

All partner endpoints

All developer docs →

Authentication

Send an API key as a bearer token. The key must carry the partner.domains permission; a key without it is refused with 403, not 404.

This endpoint takes no organisation id. Your key already identifies the organisation it belongs to, and the response is scoped to it.

Try it

Replace anything in angle brackets with your own values, and the key placeholder with a key from your dashboard.

curl -X POST https://api.zinndigital.com/v1/partner/customers/{externalId}/site-moves \
  -H "Authorization: Bearer zdk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "subscription_id": <string>, "domain": <string>, "app": <string>, "archive_url": <string>, "archive_sha256": <string> }'

Signed in? The API console in your dashboard fills in your real organisation id and your own key, and runs the request against the live API so you can see the actual response. Open this endpoint in the API console

Details

Starts an asynchronous move of ONE site into the customer's hosting plan (docs/808 §12). Nothing is charged: the move spends a site slot the customer already pays for. The call is validated and answered at once (202); the work runs as a durable workflow and is read back with getPartnerSiteMove. The archive. archive_url is an https link our hosting box downloads directly (never through this API): a .tar.gz (or .zip) holding the document root under www/ and, for a site with a database, a MySQL dump named dump.sql at the top level — the V1 backup layout. The link must answer without cookies and must survive a HEAD plus up to three GETs (a retried download fetches again). archive_sha256 is checked on the box before anything is applied; a mismatch fails the move with archive_checksum_mismatch without touching the site. The site. It is created in the customer's organization on the plan named by subscription_id, on domain, with app (a key from getPartnerCapabilities site_moves.plans[].apps). Or pass replace_site_id — one of listPartnerHostingSlots replaceable_sites — to import into that empty site and move it onto domain instead of taking a new slot. domain may be the replaced site's own name (an empty_starter is built on the customer's real domain); that is not domain_in_use. A replaced empty_starter that has content on it when the move starts fails with replace_site_not_empty, and nothing on it is touched. The domain's DNS is never touched: it keeps pointing where it points until you repoint it, and it may stay in your (or your customer's) Cloudflare for good. A subdomain whose apex is somebody else's live site is fine. Refusals (422, details[0].code): no_free_slot, subscription_not_found, app_not_supported_by_plan, plan_cannot_import, domain_invalid, domain_in_use, archive_url_invalid, archive_checksum_invalid, replace_site_not_replaceable, move_already_running, idempotency_key_required, moves_not_open, no_capacity, dns_invalid, not_linked. There is no size limit (D28455): room is made for a large archive, and the move is slower, never refused. DNS (contract v2). dns says where the zone is held and who changes it: with auto_switch: true YOU repoint the record when the move reaches step: ready_to_switch (then call go-live); otherwise the customer is shown and e-mailed the exact records, the change is detected automatically, and the move goes live on its own. A retry with the same Idempotency-Key returns the same move. Requires partner.domains.

Parameters

NameTypeRequiredWhat it is
externalId (path)stringYesYOUR id for that customer — whatever your own system calls them. It is what makes linking idempotent, and it is scoped to your partner programme: another partner's id is a 404…
Idempotency-Key (header)stringNoClient-generated key that makes an unsafe request replay-safe: the server stores the first response and returns it verbatim for repeats.

Request body

NameTypeRequiredWhat it is
subscription_idstringYesThe customer's hosting subscription the site moves onto — a subscription_id from listPartnerHostingSlots.
domainstringYesThe hostname the site serves on, apex or subdomain. Its DNS stays where it is.
appstringYesWhat the site is built with: wordpress, php (a plain PHP or static-HTML document root) or an application key the plan lists in site_moves.plans[].apps.
php_versionstringNoOptional PHP version, e.g. 8.3. Omit for the plan's default.
archive_urlstringYesThe https link to the archive. Never echoed back.
archive_sha256stringYesLower-case hex SHA-256 of the archive file, checked before it is applied.
archive_bytesintegerNoThe archive's size in bytes, when you know it.
replace_site_idobjectNoImport into this empty site instead of taking a new slot — one of listPartnerHostingSlots replaceable_sites.
source_refstringNoYour own id for the site being moved, echoed back on every read.
dnsPartnerSiteMoveDnsNoWhere the domain's zone is held and who changes it. Omitted, it is unknown and the customer-switch path applies (exact records shown and e-mailed, change detected).
sourceobjectNoFacts about the source site the post-import fix-ups use.

Response

NameTypeRequiredWhat it is
idstringYes—
statusstring<queued, importing, imported, going_live, live, failed, cancelled>Yesimported — built and answering on preview_url; switch DNS, then call goLivePartnerSiteMove. live — the domain is served by us over HTTPS; only now delete the original.
domainstringYes—
appstringYes—
subscription_idstringYes—
v2_site_idobjectYesOur site id, once the site exists.
source_refstringYes—
replaced_sitebooleanYesWhether the move imported into an existing empty site.
preview_urlstringYesAn https address that serves the imported site with no DNS change, or "" until imported.
originobjectYesWhat the domain's web record must point at. Publish an A to each a address; aaaa is empty while the box has no IPv6 — publish no AAAA then.
tlsobjectYes—
checksobjectYesWhat we measured on the preview after the import.
error_codestringYes—
errorstringYes—
created_atstringYes—
updated_atstringYes—
live_atobjectYes—
stepstring<queued, creating_site, waiting_for_space, downloading, importing_files, importing_database, checking, ready_to_switch, waiting_for_dns, switching, securing, live, failed, cancelled>NoWhere the move is, finer than status — show it with progress and elapsed_seconds so a long import never looks crashed. There is no size limit and no clock on a move's size…
progressobjectNoMeasured on the hosting box while it applies the archive. null means not known yet — never zero.
started_atstringNo—
step_started_atobjectNo—
elapsed_secondsintegerNo—
retryablebooleanNo—
dns_casestring<partner_switch, customer_switch, unknown>No—
dns_holderstringNo—
switchobjectNoEXACTLY what to publish for the domain to be served by us.
rollback_advisedbooleanNoYour switch did not take within the attempt: restore the records you saved in dns.current. The original never stopped serving.
waiting_sinceobjectNo—
reminders_sentintegerNo—
extra_disk_mbintegerNoFree disk space (MB) this move gave the site on top of its plan so the move could land whatever its size (D28455). 0 when the plan's own space was enough. Always present.
extra_disk_expires_atobjectNoWhen extra_disk_mb ends: 7 days after live_at (D28457). null while the move is still running (the extra is open-ended until it goes live) or when there is no extra.…

Errors this endpoint can return

401 · 403 · 404 · 422 · 429