Link your old site to your storefront
Keep names apart between your old site and your storefront, pass sign-ins across, and move accounts and your old forum's long threads.
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 and forum profile.
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.
Before you begin
Section titled Before you begin- 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
stateanswers404. - Coritan must have your storefront's address registered, as social sign-in needs.
gosends 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.
Sign each request
Section titled Sign each requestA request from your old site carries two headers:
X-Legacy-Timestamp- When you signed it, in Unix seconds.
X-Legacy-Signaturev1=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.
1767225600
POST
/api/v1/orgs/acme/legacy-migration/availability
<SHA-256 of the body, as lower-case hex>
In 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.
Keep names apart
Section titled Keep names apartYour 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.
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.
Pass sign-ins on to your storefront
Section titled Pass sign-ins on to your storefrontWhen 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.
Send the sign-in to
relay-login, with the person's IP address inclient_ipand their browser's user agent inuser_agent. Name the account withlogin, the email address or username as the person typed it, or withcustomer_idwhen your old site knows the customer's ID on your storefront. Send one of the two.Shellcurl -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"}'Act on
resultin the answer:ok: send the browser togo_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. Tell the person to sign in on your storefront, and link them togo_url, which opens its sign-in page.rate_limited: too many wrong passwords. Ask the person to try again later.
go_urlsends the browser to your storefront's/auth/callback, withlegacy_codein the URL fragment. Your storefront posts that code toexchange:Shellcurl -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/loginanswers: 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, and only for your organization. We create the session when the relay succeeds, so opening the same code twice within that minute gives the same session.
Follow the switches Coritan sets
Section titled Follow the switches Coritan setsCoritan opens the move and freezes your old forum when you ask support, and your old site learns both from state. Ask it at most every 30 seconds, and keep the last answer for when we cannot be reached.
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}:
move_open: offer the move on your old site, and answercheck-loginwithcan_move. While it isfalse, send a customer who signs in on your storefront with an old account to your old site, signed in. It isfalsewhile the link is off.forum_read_only: refuse new threads, replies and edits on your old forum.
state answers while the link is off, so your old site learns that the move closed. It answers 404 only while no shared secret is set.
Move an account
Section titled Move an accountA 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). go opens your storefront's /migrate.
Your storefront sends the ticket to
ticket. The answer says what the move will do, before anything moves: the oldusernameandemail, and inaccountwhich 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 and both sites verified it, so the old account joins it.merge_sign_in: an account here has the same email but one site never verified it. The customer signs in to that account first, and your storefront sends its token withredeem.blocked: the account here with that email is banned, suspended or closed.
handleproposes the username for a new account or a placeholder, withreasonwhen the old username could not be kept.terms_urlandprivacy_urllink your terms and privacy policy.The customer accepts your terms and, for a new account, may pick another username. Your storefront sends
redeemwith theticket,accept_terms: trueand thehandle:Shellcurl -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"}'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 wasmerged,credited_usd, the number ofcardssaved, and asession(or a two-factor step) shaped like the answer ofPOST /auth/login.
What moves:
- The account: the password (so the old password signs in here), the names and when the email was verified. 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.
Services, two-factor authentication, invoices and social sign-ins stay behind. A customer who signed in with Discord connects it again here.
We record that the customer accepted your terms, email them a notice of the move, 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.
Your old site answers three signed routes for us, under its own API address:
POST /internal/migration/redeemwith theticket: check it, lock the account and answer the account (including its password hash), the providers, the forum profile, the Stripe customer ID and the balances. While the account stays locked, answer the same again.POST /internal/migration/completewithold_user_id,customer_idand what wecredited: mark the account moved, set its balances to zero and report any balance that differs asdrift.POST /internal/migration/check-loginwithlogin,passwordandclient_ip: for the storefront sign-in fallback in Sign customers in to your storefront. Answerinvalid,can_movewith a ticket, orhas_serverswith a link that signs the customer in on your old site.
Report money you will not settle
Section titled Report money you will not settleWhen 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.
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.
Move your old forum
Section titled Move your old forumCoritan 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 has at least 750 characters 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 onlyhttpsimages), 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
unclaimedholds 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
@namein 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.
Result
Section titled ResultNo 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.
Troubleshooting
Section titled Troubleshooting404 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.
availabilityneedsemailorusername, andrelay-loginneeds exactly one ofcustomer_idandlogin. 404 Customer not found- The
customer_idyou sent toalertsis not a customer of your organization. 400 That sign-in link expired. Sign in again.- The
legacy_codeis more than a minute old, belongs to another organization, or was changed on the way. Sign in again on your old site. 400withinvalid_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.
401withsign_in_required- An account here has the customer's email and one site never verified it. The customer signs in to that account, then your storefront sends
redeemwith its token. 409withhandle_unavailable,not_eligible,already_moved,email_blockedorlimit_reached- The username is taken (the answer has a
suggestion), the old account still has whatblockinglists, the account already moved, the email belongs to a blocked account here, or your organization holds as many customers as it may. 503withold_site_unavailableorrate_unavailable- Your old site did not answer, or we have no fresh SOL rate. Nothing moved; the customer tries again later.
Related
Section titled RelatedAPI operations on this page
| Method | Path | What it does |
|---|---|---|
POST | /api/v1/orgs/{org_slug}/legacy-migration/availability | Whether an email address or a username is already taken in the organization |
POST | /api/v1/orgs/{org_slug}/legacy-migration/relay-login | A sign-in typed on the previous platform for an account that lives here |
POST | /api/v1/orgs/{org_slug}/legacy-migration/alerts | Money or a chargeback the previous platform will not settle for a moved account |
POST | /api/v1/orgs/{org_slug}/legacy-migration/state | Whether the previous platform's move is open and its forum is frozen |
GET | /api/v1/orgs/{org_slug}/legacy-migration/go | Send the browser to the organization's storefront |
POST | /api/v1/orgs/{org_slug}/legacy-migration/exchange | Open a relayed sign-in code and return the session it holds |
POST | /api/v1/orgs/{org_slug}/legacy-migration/ticket | What moving an account from the previous website will do, before anything moves |
POST | /api/v1/orgs/{org_slug}/legacy-migration/redeem | Move an account from the previous website into the organization |