Skip to content
Coritan Docs

Link your old site to your storefront

Keep names apart between your old site and your storefront, pass sign-ins across, and move accounts, free servers and your old forum's long threads.

View as Markdown

If your organization ran its own site before its Coritan storefront, Coritan can link the two while your customers move across. Your old site then checks with your storefront before it gives out a username or an email address. A person who signs in on the old site with an account that lives on your storefront arrives there, signed in with the details they typed. A customer with no services left on the old site can move their account, with its balance, saved cards, forum profile and the Discord, GitHub, Google or Microsoft accounts they sign in with.

Every route on this page is under https://api.coritan.com/api/v1/orgs/{org_slug}/legacy-migration. Your old site's server calls availability, relay-login, state and alerts, and signs each request. A browser opens go, and your storefront calls exchange, ticket and redeem. Coritan calls routes on your old site too, under its own API address: check-login, taken, and the routes that move accounts. It signs those requests the same way.

  • Ask support to link your old site. Coritan sets up the link with a secret that you and Coritan share, gives you the secret with Coritan's API address and your organization's slug for your old site's server, then turns the link on. Until it is on, every route on this page but state answers 404.
  • Coritan must have your storefront's address registered, as social sign-in needs. go sends people only to that address.
  • Keep the shared secret on your old site's server. Anyone who holds it can act as your old site.

A request from your old site carries two headers:

X-Legacy-Timestamp
When you signed it, in Unix seconds.
X-Legacy-Signature
v1= followed by the HMAC-SHA256 of the message below, made with the shared secret and written as lower-case hex.

The message is four lines joined by a newline, with no newline at the end: the timestamp from the header, the method in capitals, the path (with ? and the query string when the URL has one), and the SHA-256 of the raw body as lower-case hex.

The message for a request to availability
1767225600
POST
/api/v1/orgs/acme/legacy-migration/availability
<SHA-256 of the body, as lower-case hex>

In Python:

Python
import hashlib, hmac, json, time

def signed_headers(secret: str, method: str, target: str, body: bytes) -> dict:
    timestamp = str(int(time.time()))
    digest = hashlib.sha256(body).hexdigest()
    message = f"{timestamp}\n{method.upper()}\n{target}\n{digest}".encode()
    mac = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return {"X-Legacy-Timestamp": timestamp, "X-Legacy-Signature": f"v1={mac}"}

body = json.dumps({"email": "alex@example.com"}).encode()
headers = signed_headers(SECRET, "POST", "/api/v1/orgs/acme/legacy-migration/availability", body)

Send exactly the bytes you signed, with Content-Type: application/json. We refuse a request whose signature does not match, or whose timestamp is more than 5 minutes from our clock. We sign our own requests to your old site the same way, and your old site should refuse one that does not check out.

Your old site asks before it registers an account, and before it changes an account's email address or username. When the answer says a value is taken, refuse it, so that nobody holds a name on one site that belongs to someone else on the other.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/legacy-migration/availability" \
  -H "Content-Type: application/json" \
  -H "X-Legacy-Timestamp: 1767225600" \
  -H "X-Legacy-Signature: v1=..." \
  -d '{"email": "alex@example.com", "username": "alex_builds"}'

Send email, username or both. The answer is {"email_taken": false, "username_taken": true}. Both compare without regard to letter case, and a value is taken when any customer of your organization holds it, whatever that customer's status.

If your old site cannot reach us, refuse the sign-up or the change: a value we could not check may already be taken here.

The check runs the other way too. While the link is on and its sign-in fallback is on, a sign-up on your storefront asks your old site's POST /internal/migration/taken with the email, the username or both, and refuses with 409 and code set to account_on_old_site when your old site holds either. The customer then signs in with their old username and password, which offers the move. Answer {"email_taken": true, "username_taken": false}. A value counts as taken only while an old account that has not moved holds it: an account that moved is an account on your storefront, and availability answers for it. Compare without regard to letter case. When your old site does not answer within 5 seconds, or answers outside this shape, the sign-up goes ahead.

Pass sign-ins on to your storefront

Section titled Pass sign-ins on to your storefront

When someone signs in on your old site with an account that lives on your storefront, your old site passes the sign-in to us instead of checking its own copy of the password. That covers an account your old site marks as moved, and a login your old site does not know, such as an account made on your storefront.

  1. Send the sign-in to relay-login, with the person's IP address in client_ip and their browser's user agent in user_agent. client_ip must be an IPv4 or IPv6 address, or the request answers 422. Behind a proxy, send the address your proxy fixed for the connection, never the first X-Forwarded-For value, which the client writes itself. Name the account with login, the email address or username as the person typed it, or with customer_id when your old site knows the customer's ID on your storefront. Send one of the two.

    Shell
    curl -X POST "https://api.coritan.com/api/v1/orgs/acme/legacy-migration/relay-login" \
      -H "Content-Type: application/json" \
      -H "X-Legacy-Timestamp: 1767225600" \
      -H "X-Legacy-Signature: v1=..." \
      -d '{"login": "alex@example.com", "password": "a long passphrase", "client_ip": "203.0.113.10", "user_agent": "Mozilla/5.0"}'
    
  2. Act on result in the answer:

    • ok: send the browser to go_url.
    • invalid: show your usual message for a wrong password. We answer the same way for a login we do not know.
    • social_only: the account has no password. The answer also has providers, the sign-in providers linked to the account (discord, github, google or microsoft, in that order, and an empty list when there are none), and has_password set to false. Tell the person which provider to use on your storefront, and link them to go_url, which opens its sign-in page.
    • rate_limited: too many wrong passwords. Ask the person to try again later.
  3. go_url sends the browser to your storefront's /auth/callback, with legacy_code in the URL fragment. Your storefront posts that code to exchange:

    Shell
    curl -X POST "https://api.coritan.com/api/v1/orgs/acme/legacy-migration/exchange" \
      -H "Content-Type: application/json" \
      -d '{"code": "..."}'
    

    The answer has the shape POST /auth/login answers: a session, or a two-factor step to finish as Two-factor authentication describes.

We check the password as POST /auth/login does, so only active customers can sign in this way. Wrong passwords count against the person's IP address, in the same budget as your storefront's sign-in form, and against the account: one account takes at most 10 wrong passwords in 15 minutes. The session records the person's IP address and browser, as it would if they had signed in on your storefront.

A legacy_code opens for 1 minute, only for your organization, and only once: we create the session when the relay succeeds, and a second open of the same code answers 400, on any of our servers. We also write customer.login_via_old_site to your audit log for each sign-in we accept, with the person's IP address from client_ip.

Follow the switches Coritan sets

Section titled Follow the switches Coritan sets

Coritan opens the move, freezes your old forum, closes sign-ups on your old site and turns on the free-server move when you ask support, and your old site learns all four from state. Ask it at most every 30 seconds, and keep the last answer for when we cannot be reached.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/legacy-migration/state" \
  -H "Content-Type: application/json" \
  -H "X-Legacy-Timestamp: 1767225600" \
  -H "X-Legacy-Signature: v1=..." \
  -d '{}'

The body is an empty object, and the answer is {"move_open": true, "forum_read_only": false, "signups_closed": false, "server_moves_open": false}:

  • move_open: offer the move on your old site, and answer check-login with can_move. While it is false, send a customer who signs in on your storefront with an old account to your old site, signed in. It is false while the link is off.
  • forum_read_only: refuse new threads, replies and edits on your old forum.
  • signups_closed: refuse new accounts and new free servers on your old site, and send people to sign up on your storefront. Keep letting existing accounts sign in, and leave paid checkout, renewals and invoices as they are. It is false while the link is off, and your old site treats a missing answer as false.
  • server_moves_open: staff are moving free servers to your storefront. Keep stopping and suspending idle free servers, but stop sending the warning and stop deleting them, so each one waits for its move (Move free servers). It is false while the link is off, and your old site treats a missing answer as false.

state answers while the link is off, so your old site learns that the move closed and that the sign-up and free-server switches are off. It answers 404 only while no shared secret is set.

A customer starts the move on your old site: they confirm their password there, and your old site checks that they have no servers or subscriptions left. It then sends the browser to go with to=migrate and a move ticket in the fragment: https://api.coritan.com/api/v1/orgs/{org_slug}/legacy-migration/go?to=migrate#ticket=.... The ticket is a JWT your old site signs with the shared secret (HS256), valid for 10 minutes, naming the old account's ID (sub), username, email, email_verified and the social sign-ins it used (providers). email_verified is true only when the old account has the verified flag and the date it was verified: a flag with no date, such as one a social sign-up sets without checking the address, is false. go opens your storefront's /migrate.

  1. Your storefront sends the ticket to ticket. The answer says what the move will do, before anything moves: the old username and email, and in account which case applies:

    • new: nobody here uses the email, so the move makes an account.
    • placeholder: the account's forum threads already moved with your forum, so the move turns their placeholder into the account.
    • merge_automatic: an account here has the same email, the customer confirmed it here, and your old site really verified it (the ticket's email_verified is true), so the old account joins it.
    • merge_sign_in: an account here has the same email but one site never confirmed it. The customer signs in to that account first, and your storefront sends its token with redeem. redeem decides again from the old account itself, so a ticket that said merge_automatic still answers sign_in_required when the old account has the flag and no date.
    • blocked: the account here with that email is banned, suspended or closed.

    handle proposes the username for a new account or a placeholder, with reason when the old username could not be kept. terms_url and privacy_url link your terms and privacy policy.

  2. The customer accepts your terms and, for a new account, may pick another username. Your storefront sends redeem with the ticket, accept_terms: true and the handle:

    Shell
    curl -X POST "https://api.coritan.com/api/v1/orgs/acme/legacy-migration/redeem" \
      -H "Content-Type: application/json" \
      -d '{"ticket": "...", "accept_terms": true, "handle": "alex_builds"}'
    
  3. We ask your old site to lock the account and send what moves, write it here in one go, then tell your old site the move is done. The answer holds the handle, whether the account was merged, credited_usd, the number of cards saved, and a session (or a two-factor step) shaped like the answer of POST /auth/login.

What moves:

  • The account: the password (so the old password signs in here, when the account has one), the names and when the email was verified. A moved account counts its address as confirmed only when your old site sent both the flag and the date: we never prove an address that your old site did not. A merge keeps the existing account's password, username and settings.
  • The balance, as one wallet credit in USD: the old USD balance, USDC and USDT at face value, and SOL at our receive rate for SOL, rounded half up to the cent. The credit is not money collected, so it counts towards neither your revenue nor your payouts, and an old account is credited only once.
  • Saved cards, when your old site used the Stripe account your storefront charges through and the account here has no Stripe customer of its own yet. A Stripe error does not stop the move.
  • The forum profile, and the threads of a placeholder.
  • The sign-in links: each Discord, GitHub, Google or Microsoft account the customer used on your old site. Both sites read the same provider user ID for a person, so we store the old site's ID as it is, with no token. The customer signs in with that provider on your storefront and lands in the moved account, with no password and no connecting again. A provider your storefront does not offer yet works once you switch it on.

A link stays behind in two cases, and the move still succeeds. Another account on your storefront already uses that Discord, GitHub, Google or Microsoft account, or a Discord account that moved between accounts too often (linked_elsewhere). Or the customer's account already had a link for that provider, as a merged account can (provider_already_connected). We never replace or take over a link that exists. Your staff see each left-behind link, with its reason, on the customer's page, and the customer connects that provider again from their account settings. For linked_elsewhere, the page also names the account that holds the link and says whether that account looked empty when the move ran, which usually means that signing in with the provider made a second account for the same person (Read a customer's account).

Services, two-factor authentication and invoices stay behind.

We record that the customer accepted your terms, email them a notice of the move that names the sign-in links that came across, and write customer.moved_from_legacy to your audit log. When the old balance changed after the lock, we write a customer.legacy_alert for your staff. When your old site does not confirm the move, your staff can ask it again from the customer's page (Confirm a move with the old site).

Sending the same ticket again answers the same result and credits nothing twice. The repeat carries a session only when all of these hold. The ticket was issued before the move. The move is at most 5 minutes old. No session that started at or after the move has been revoked: signing out, changing or resetting the password, and staff ending sessions all count. And the account's password has not changed since the move. Otherwise the answer is done with session set to null, and the customer signs in.

Addresses the old site did not confirm

Section titled Addresses the old site did not confirm

A move that brings sign-in links to an account whose address is not confirmed here, because your old site sent no verified date, records the providers it brought as sign-in links to remove. The person who proves the address may not own those provider accounts. The first proof of the address by email removes them: a sign-in code, the confirmation link, a password link, or your staff marking the address verified. We write one customer.oauth_links_cleared entry to your audit log, with the provider names, and the customer connects each provider again after signing in with their email address.

A provider that says it verified the account's own address proves it by signing in, and keeps its link. Until the address is proven, the account cannot connect a provider of its own. Social sign-in says which providers vouch for an address. The same rule covers the sign-in links that your staff bring over for accounts that moved earlier.

The routes your old site answers

Section titled The routes your old site answers

Your old site answers four signed routes for us, under its own API address:

  • POST /internal/migration/redeem with the ticket: check it, lock the account and answer the account (including its password hash), the providers, the sign-in links in connections, the forum profile, the Stripe customer ID and the balances. Each link is {"provider", "id", "email", "username"}, where id is the old site's provider user ID as a string and there is never a token. The account holds email_verified and email_verified_at: send email_verified as true only when the flag is set and email_verified_at says when, and leave the date out when the flag is off. While the account stays locked, answer the same again.
  • POST /internal/migration/complete with old_user_id, customer_id and what we credited: mark the account moved, set its balances to zero and report any balance that differs as drift. Once the account is moved, clear its password hash, two-factor secret, remember token, provider tokens, API keys and recovery tokens. Do the same for a repeated complete for an account that is already moved, and answer ok again.
  • POST /internal/migration/check-login with login, password and client_ip: for the storefront sign-in fallback in Sign customers in to your storefront. Answer invalid, can_move with a ticket, has_servers with a link that signs the customer in on your old site, moved with the customer's ID when the typed name is an old username of an account that moved, or rate_limited. We follow a has_servers link only when it is an https address on your old site's own domain.
  • POST /internal/migration/taken with email, username or both: answer email_taken and username_taken, as Keep names apart says.

What a storefront built on these routes shows

Section titled What a storefront built on these routes shows

A storefront we build has a /migrate page built on ticket and redeem that gives each state its own words. Use it as a model for yours.

  • Opened with no ticket, it says "Start from the old site": open the move link on the old site, or sign in with the old username and password, which brings the person back with a new ticket.
  • When the ticket is wrong or past its 10 minutes (invalid_ticket), it says "This link has expired" and offers the same two ways on.
  • For a merge, it says which account the old one joins and why the customer signs in first: "We ask so that only its owner can add to it." It tells them to use the sign-in details of that account, and that Forgot password is on the sign-in page.
  • The sign-in providers the account used show as links that come along: "We bring your Discord sign-in with your account, so you can use it here." A merged account's provider joins the account here unless that account already has one of its own, and a provider the storefront does not offer yet reads "not available on this site yet".
  • When the account has already moved and the repeat carries no session, it says "Your account is already moved" and "Sign in with your password. If you have no password yet, use Forgot password to choose one." It adds that choosing a password by email confirms the address, which can remove sign-in connections that came from the old site.
Section titled Bring over the sign-in links of accounts that moved earlier

Accounts that moved before your old site reported sign-in links have none here yet. Ask support to bring them over, once your old site answers POST /internal/migration/connections.

That route takes {"old_user_ids": [12, 13]}, up to 200 IDs, and answers each with a state and its connections. state is moved when the old account's external_id is coritan: and the customer's ID, pending when it is coritan:pending, not_moved for anything else (an empty external_id too) and missing when no such user exists. Return the links only for moved and pending: an account that has not moved never has its links read.

Coritan's staff run it from the organization's admin page, with no ticket and no person involved. A dry run checks up to 200 waiting accounts and reports what would happen without writing anything: links it would write, links already there, links left alone with their reasons, and accounts your old site does not show as moved. The real run repeats 200 accounts at a time until none wait. It stamps each account it checks, so running it again writes nothing twice, and it never replaces a link.

Move accounts for your customers

Section titled Move accounts for your customers

Coritan's staff can also move accounts with nobody present. Ask support to run it. A person then finds their account, balance, saved cards and sign-in on your storefront without doing anything.

One rule no setting changes. Only an account that owns no server, has no subscription that is active, suspended or unpaid, and owes nothing can move this way. An account with servers moves only in a free-server move, and only when every one of its servers is free. Your old site checks the rule when Coritan reads the candidates, and checks it again under a lock when it takes each account, so an account that ordered a server a minute after the preview stays where it is.

Coritan's staff choose the accounts with parameters, and all of them are optional:

  • How many, and in what order. The oldest or newest accounts, the ones quiet the longest, the most recently active, or the highest balance. "The 1,000 oldest" is a run. Running it again picks the next 1,000, because accounts that moved no longer qualify.
  • Dates. Created before or after a date, and last signed in before or after a date, or never.
  • Email. Verified or not, and the email domains to include or leave out.
  • Sign-in. Has a password or not, has a Discord, GitHub, Google or Microsoft link or none.
  • Money. Has a balance, a minimum balance, or a saved card. Wrote forum threads.
  • Special cases. Accounts staff disabled on the old site move as banned, so their addresses stay blocked, and only when staff ask for them. An account that helps on someone else's server, as a subuser, stays out unless staff ask, because moving it ends its access to that server.
  • A list. Old user IDs or email addresses, up to 1,000. Only those that qualify move.

Staff preview the run before they start it. The preview counts the matches and shows how they would land: a new customer, a forum placeholder claimed, an automatic merge into a customer here, an account that waits for its owner (the address is not confirmed here, or your old site did not really verify it, so merging would hand one person's account to another), and an account blocked here (its address belongs to a banned, suspended or closed customer). It adds the credit, the sign-in links, the number of emails and how many go to unverified addresses. Starting needs the organization's slug and the preview's count typed, and the list of accounts is fixed at that moment.

A job then moves the accounts in batches of 50. For each batch it asks your old site for the candidates (GET /internal/migration/candidates, where each row carries email_verified and email_verified_at), takes only the ones your storefront can take, locks them (POST /internal/migration/bulk-redeem), takes each account in, and confirms (POST /internal/migration/bulk-complete). Your old site answers all three with the signed requests described above, and the same fixtures hold both sides to the same messages. Staff can pause, resume or cancel a run. An account locked on the old site that your storefront could not finish is shown as locked, retried, and finished by "Retry the ones left locked". Its owner can also sign in on the old site, which sends them to move it themselves.

A moved account differs from a self-service move in four ways. Nobody accepts your terms, so no terms acceptance is recorded, and a customer accepts them at the first order. No sign-up IP address or browser fingerprint is recorded. The customer's created date is the old account's. An account with no password stays without one: its owner signs in with a moved provider link, or sets a password from the link in the email. The staff console shows "Moved by staff in run #N" on the customer's page.

The email each moved person gets

Section titled The email each moved person gets

Each moved person gets the legacy_account_moved email, unless staff chose not to email, or only verified addresses. The default email says that the account moved, what came with it and how to sign in, and the sentence about signing in depends on the account:

  • An account with a password: sign in with the email address and the password from the old site. A merged account keeps its own password, and the email says so.
  • No password, and a moved provider link that your storefront offers: sign in with that provider. The email adds that a password can be added in the account settings when the address is confirmed, and otherwise points to Forgot password on the sign-in page.
  • No password and no provider link, in a move that staff ran: a "Set your password" button that works for 30 days. After that, opening the link sends a new one.
  • Any other account with no password and no provider link: Forgot password on the sign-in page.

The email never tells a customer with no password to sign in with the old one. The template's context has no_password and address_proven for this. Your organization can replace it in its email templates, for example with a version that also tells the customer what is new. Because it carries news, it includes the organization's legal name and postal address, a link to stop news and offers, and the one-click unsubscribe header that mail apps use. The link opens a page that changes nothing until the customer presses its button. Stopping news never stops emails about the customer's account or services. Emails go out slowly, at most 2,000 an hour, and after the moves that earn them.

An account with a server cannot move itself: the old site's move page lists each server as something to delete first. Coritan's staff can move the free servers instead, with nobody present, and the accounts that own them. Ask support to run it. Paid servers stay on your old site, and so does any account that owns one.

The move has its own switch, Move free servers, under Previous site in the organization's settings. Staff turn it on before a run can start, and a start while it is off answers 409 with switch_off. While it is on, your old site keeps stopping idle free servers but no longer sends the warning and no longer deletes them, so each one waits for its move. Your old site learns the switch from server_moves_open in state, within a minute. Turning it off lets your old site delete idle free servers again.

Each server goes to the location your storefront sells that matches the old location. Staff see the map as Where servers go, one row for each old location, and can change each row to any location your storefront sells servers in. The defaults are lhr to lhr, fra to fra, sgp to sin and ewr to iad. An account is skipped, and never locked on your old site, when a server's location has no place here (unmapped_location) or its software is one your storefront does not offer (unsupported_software).

Which software each server becomes

Section titled Which software each server becomes

Your old site makes every Java server from one Java egg, so the egg does not say what the server runs. When an account is taken in, your old site reads the server's files and tells us:

  • A plugins folder means Paper.
  • A mods folder means Forge, NeoForge or Fabric, by what the server's latest log names.
  • A server with neither folder becomes Paper, the software your old site installs for the Java egg.
  • A Bedrock server becomes Bedrock. Your old site does not read its files.

The new server gets that software, on the Minecraft version of the old server: the version its latest log names, else the version it was set to, else the version it last reported, else the latest. Older egg names, such as java.Fabric, still map to the same software when your old site does not send the software.

An account stays on your old site, and nothing on it is locked, when a Java server has a mods folder and its latest log names none of Forge, NeoForge or Fabric. The same holds for a server that runs software your storefront does not offer. The preview does not read files, so its By software table counts a Java server as Paper and says that the loader is read when each account is taken in. The skip shows in the run as unsupported_software.

Mods and plugins load only when the loader matches. A Paper server loads plugins and a Forge, NeoForge or Fabric server loads mods for its own loader, so a mod built for Forge does not load on Fabric. An account whose loader the log does not name stays behind, because a wrong guess would leave its mods unloaded.

An account qualifies when all of these hold:

  • It owns at least one server, and every server it owns is free. One paid server keeps the account on your old site (old_refused_paid_server).
  • It has no subscription that is active, suspended or unpaid, and it owes nothing.
  • Every server has had no players for at least the number of days staff chose, 7 by default. A server with players more recently stays (old_refused_active_server).
  • A server with no player record is left out unless staff include those servers (old_refused_untracked_server). Your old site tracks a server only when it has a subdomain, so one without a subdomain has no record.
  • The usual rules of an account move hold: it is not an administrator, it is not already moved, and it is not disabled or a subuser unless staff ask for those.

Staff choose the accounts by order (the oldest, the newest, the quietest or the most recent), a limit, the dates the account was created and last signed in, or a list of old user IDs or email addresses, up to 1,000. They also set the days without players, whether to include servers with no player record, whether to bring the old backups, and how many accounts to take each minute (10 by default). They preview first. The preview counts the accounts and the servers, adds the old backups and their size, groups the servers by old location and by software, and says how many accounts the map or the software would skip. Starting needs the organization's slug and the preview's count typed. Only one free-server move can run at a time, and it can run beside an account move.

  1. Your old site locks the account, stops each of its servers and marks each as moving. From then on it refuses every start of that server.
  2. Coritan takes the account in as an account move does, and creates a new free server for the customer in the mapped location, with the mapped software and version. The server is held while it moves: nobody can start it, change its files or take a snapshot of it, and the panels show it as moving.
  3. Your old site packs the server's files into one archive. Coritan copies the archive into a snapshot named "Moved from the old site", then restores that snapshot onto the new server and checks that the world is there. A free server may not run an uploaded program, so Coritan puts back the server software its own install provides.
  4. Each backup the old site holds becomes a snapshot with the backup's name, shown as "Old site backup", unless staff turned the backups off.
  5. Your old site records the move and the hold lifts. The new server is stopped, and the customer starts it when they are ready.

The snapshots are the customer's: they count in their snapshot storage, and the customer can restore, download or delete them like any other snapshot. Coritan keeps the customer's snapshots out of automatic cleanup for 90 days. A person the old server was shared with gets their access back when their own account has moved.

Nothing on your old site closes until the restore is checked. After a verified move your old site keeps the old server, stopped, and deletes it 14 days later. The join address in the email is the new server's join name when it has one, and its IP address and port otherwise.

When every server of an account has moved, the person gets one email, legacy_account_moved, that says the account moved and names each server with its join address and how many snapshots it came with. The email rules in The email each moved person gets apply, and staff can send it to everyone, only to verified addresses, or to no one.

Staff can pause, resume or cancel a run, and retry what failed. Cancelling admits no more accounts, marks the servers still waiting as cancelled and stops a server that is moving at its next step. The run also pauses itself, and says why, when:

  • no node has room for a free server (no_capacity);
  • a location has no snapshot storage (no_snapshot_storage);
  • your old site does not answer (old_site_unavailable);
  • 20 servers fail in a row (too_many_failures).

A server gets up to 5 attempts before it is marked failed, and staff retry it after they fix the cause. The run copies at most 8 servers at once for the whole platform and 2 for each old node, so a large run takes time and your old site is not flooded.

Staff can give any server that has not finished moving back to your old site. They open the server in the run, choose Give back and type the old server's ID. Your old site then lets the server start again, restores what the move changed about it, and Coritan deletes the copy it made. The account stays locked on your old site. A server that finished moving, or that staff already gave back, cannot be given back again.

Report money you will not settle

Section titled Report money you will not settle

When a payment or a deposit reaches an old account after the account moved to your storefront, or a customer disputes a charge from your old site, tell us with alerts. We credit and deduct nothing. We write the alert to your organization's audit log as customer.legacy_alert, and your staff see it on the customer's page in the staff console, to settle by hand.

Shell
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/legacy-migration/alerts" \
  -H "Content-Type: application/json" \
  -H "X-Legacy-Timestamp: 1767225600" \
  -H "X-Legacy-Signature: v1=..." \
  -d '{"customer_id": 4812, "old_user_id": 90211, "kind": "late_money", "source": "stripe", "amount_cents": 1500, "currency": "USD", "reference": "pi_3Q", "note": "Top-up paid after the move"}'

customer_id is the customer on your storefront, and old_user_id the account on your old site. kind is late_money or chargeback, and source is stripe, paypal, paynow, crypto or other. reference and note are optional.

Coritan can move your old forum into your storefront's community once. Ask support: Coritan freezes the old forum first, through forum_read_only in state, so nobody posts after the move. It then reads the forum from your old site over the same signed link, a page at a time.

  • A thread moves when its opening post reaches the minimum length set for the run (750 characters unless Coritan sets another) once converted to Markdown. It moves as that opening post alone, without replies, with its board, title, tags, view count, dates and its sticky and locked flags. Your forum needs a board with the same slug as the old one, or the thread is skipped.
  • Before anything is written, Coritan sends you a dry run's report: the threads that qualify, the ones the moderation filter would refuse, images linked over plain http (your storefront shows only https images), and slugs that had to change. Read it before the real run.
  • An author who already moved their account here is the thread's author. For anyone else, a placeholder with the status unclaimed holds their forum handle, avatar, banner and role, and their old username as the display name when it differs from the handle. A placeholder cannot sign in, and Manage customers lists what else it refuses. When that person moves their account, the placeholder becomes their account and keeps their threads.
  • An @name in a moved post no longer links anyone, because the name may belong to someone else here now.

Old links keep working where a rule can find the new page. A thread whose slug was longer than 140 characters redirects to the slug cut to 140. A thread whose old slug ended in a hyphen, or held a run of hyphens or an underscore, redirects to the slug with those tidied into single hyphens. /community/users/{old name} redirects to the profile that name became when the old username was not a valid handle. Threads that did not move answer 404.

No name is held on both sites by two different people. A person who signs in on your old site with an account on your storefront arrives on your storefront signed in, or at its two-factor step.

404 Not found
The link is off, or its secret or your old site's address is missing. From go, it can also mean Coritan has no storefront address registered for your organization. Ask support to check the link.
401 Invalid signature
The signature does not match, or the timestamp is more than 5 minutes from our clock. Sign the exact bytes you send, include ? and the query string in the path you sign when the URL has one, and keep your server's clock right.
422 The body does not match the contract
A field is missing or has the wrong type. availability needs email or username, and relay-login needs exactly one of customer_id and login, and a client_ip that is an IPv4 or IPv6 address.
404 Customer not found
The customer_id you sent to alerts is not a customer of your organization.
400 That sign-in link expired. Sign in again.
The legacy_code is more than a minute old, was opened already, belongs to another organization, or was changed on the way. Sign in again on your old site.
400 with invalid_ticket
The move ticket was changed, is of another kind, or is more than 10 minutes old. The customer starts the move again on your old site.
401 with sign_in_required
An account here has the customer's email and the move cannot join them on a ticket alone: the customer never confirmed the address here, or your old site sent no verified date for it. The customer signs in to that account, then your storefront sends redeem with its token.
409 with handle_unavailable, not_eligible, already_moved, email_blocked or limit_reached
The username is taken (the answer has a suggestion), the old account still has what blocking lists, the account already moved, the email belongs to a blocked account here, or your organization holds as many customers as it may.
503 with old_site_unavailable or rate_unavailable
Your old site did not answer, or we have no fresh SOL rate. Nothing moved; the customer tries again later.

API operations on this page

MethodPathWhat it does
POST/api/v1/orgs/{org_slug}/legacy-migration/availabilityWhether an email address or a username is already taken in the organization
POST/api/v1/orgs/{org_slug}/legacy-migration/relay-loginA sign-in typed on the previous platform for an account that lives here
POST/api/v1/orgs/{org_slug}/legacy-migration/alertsMoney or a chargeback the previous platform will not settle for a moved account
POST/api/v1/orgs/{org_slug}/legacy-migration/stateLegacy state
GET/api/v1/orgs/{org_slug}/legacy-migration/goSend the browser to the organization's storefront
POST/api/v1/orgs/{org_slug}/legacy-migration/exchangeOpen a relayed sign-in code and return the session it holds
POST/api/v1/orgs/{org_slug}/legacy-migration/ticketWhat moving an account from the previous website will do, before anything moves
POST/api/v1/orgs/{org_slug}/legacy-migration/redeemMove an account from the previous website into the organization