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 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 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.

  • 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.

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. 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. Tell the person to sign in 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, 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 sets

Coritan 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.

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}:

  • 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.

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.

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). 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 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 with redeem.
    • 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), 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/redeem with the ticket: 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/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.
  • 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, or has_servers with a link that signs the customer in on your old site.

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 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 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.
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, 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 one site never verified 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/stateWhether the previous platform's move is open and its forum is frozen
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