Skip to content
Coritan Docs

Make your first API request

Create an API key, list your services with it, and sign in for the few routes a key cannot reach, with curl commands you can copy.

View as Markdown

In the dashboard

This tutorial takes you from the dashboard to a working API request in a terminal. You create an API key, list your services with it and see what a key cannot do. For those few routes, you sign in with your password and renew the access token when it runs out. Every request goes to https://api.coritan.com/api/v1.

  • A Coritan account. Create an account if you have none.
  • curl, and jq to read values out of the JSON answers.
  • For sections 4 and 5 only: your password, and your authenticator app if you turned on two-factor authentication.

A script sends an API key to prove it acts for you. The key works until you revoke it, so the script never needs your password.

  1. Sign in to the dashboard. In the sidebar, select Settings, then the API keys tab.

  2. Select Create API key…. On the Create an API key page, type a Label, such as First script.

  3. Keep the Preset the page starts with. It is Read only, so the key can read your account but cannot change it.

  4. Select Create API key, then copy the key from Copy this key now. Coritan shows it only this once. Select I have saved it when you have copied it.

  5. In your terminal, read the key into a variable, so it stays out of your shell history:

    Shell
    read -rs -p "API key: " CORITAN_TOKEN; echo; export CORITAN_TOKEN
    

echo "${CORITAN_TOKEN:0:3}" now prints ct_. The examples across these docs send $CORITAN_TOKEN, so they work with your key as they are. Manage API keys covers the permissions and the IP allow-list in full.

Send the key in the Authorization header of every request. GET /services/ returns a JSON array of the services on your account, newest first. This prints one line for each of the first five:

Shell
curl -s "https://api.coritan.com/api/v1/services/?limit=5" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  | jq -c '.[] | {id, hostname, status, product_name}'
JSON
{"id":1042,"hostname":"web-1.example.com","status":"active","product_name":"Cloud Compute 2 GB"}

An account with no services prints nothing. Manage your subscriptions lists the filters and fields.

You now have a script that reads your account with a key.

Your key can only read, so a request that would change something answers 403:

JSON
{"detail": {"code": "api_key_read_only", "message": "This API key can only read. Create a key with a write permission to make changes."}}

A key with a Write permission can make every change you can. Whatever its permissions, a key cannot reach your profile, password, sessions, two-factor authentication, API keys or billing profile, your team, webmail or your organizations. Ask for your profile with the key to see the answer:

Shell
curl -s https://api.coritan.com/api/v1/auth/me \
  -H "Authorization: Bearer $CORITAN_TOKEN"
JSON
{"detail": {"code": "api_key_route_refused", "message": "An API key cannot read or change your profile, password, sessions, two-factor authentication, email address, notifications, API keys or billing profile, and it cannot close your account. Sign in to the dashboard for that."}}

A key also cannot work in another account you joined with the X-Coritan-Account header. What no key can do lists every route a key cannot reach. For those, use the dashboard, or sign in from the script as the next section shows.

4. Sign in for what a key cannot reach

Section titled 4. Sign in for what a key cannot reach

Signing in returns an access token. It works on every route a key works on and on the routes a key cannot reach. It expires after expires_in seconds and signing in needs your password, so use it only for what a key cannot do.

  1. The sign-in page can ask for a bot check. Ask the API whether it is on:

    Shell
    curl https://api.coritan.com/api/v1/auth/turnstile
    
    JSON
    {"enabled": false, "site_key": ""}
    

    When enabled is false, an email and a password are enough to sign in. When it is true, POST /auth/login also needs a turnstile_token that only the check on the sign-in page produces. Without one it answers 403 with {"detail": {"error": "turnstile_failed", "message": "Verification required"}}, so a script cannot sign in on its own.

  2. Read your password into a variable, so it stays out of your shell history:

    Shell
    read -rs -p "Password: " CORITAN_PASSWORD; echo
    
  3. Sign in with POST /auth/login. jq builds the JSON body, so a password with quotes or other special characters is sent intact:

    Shell
    curl -s -X POST https://api.coritan.com/api/v1/auth/login \
      -H "Content-Type: application/json" \
      -d "$(jq -n --arg email alex@example.com --arg password "$CORITAN_PASSWORD" '{email: $email, password: $password}')" \
      > login.json
    

    Without two-factor authentication, login.json holds your tokens:

    JSON
    {
      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "token_type": "bearer",
      "expires_in": 1800
    }
    

    expires_in is how many seconds the access token lasts.

  4. If you use two-factor authentication, the answer asks for your code instead:

    JSON
    {"mfa_required": true, "mfa_setup_required": false, "mfa_token": "eyJhbGciOiJIUzI1NiIs...", "expires_in": 600, "token_type": "bearer"}
    

    Send the code from your app with POST /auth/mfa/verify within ten minutes, with mfa_token as the bearer token. A recovery code works too, once. The answer holds the same tokens as a sign-in without a second factor:

    Shell
    MFA_TOKEN=$(jq -r .mfa_token login.json)
    curl -s -X POST https://api.coritan.com/api/v1/auth/mfa/verify \
      -H "Authorization: Bearer $MFA_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"code": "123456"}' \
      > login.json
    
  5. Keep both tokens in variables. The access token takes the key's place in CORITAN_TOKEN:

    Shell
    export CORITAN_TOKEN=$(jq -r .access_token login.json)
    CORITAN_REFRESH=$(jq -r .refresh_token login.json)
    

    echo "$CORITAN_TOKEN" now prints a long string that starts with eyJ. Keep login.json private or delete it: anyone with these tokens can use your account.

  6. Ask for your profile again. GET /auth/me now returns the account the token belongs to:

    Shell
    curl -s https://api.coritan.com/api/v1/auth/me \
      -H "Authorization: Bearer $CORITAN_TOKEN"
    
    JSON
    {
      "id": 4821,
      "email": "alex@example.com",
      "first_name": "Alex",
      "last_name": null,
      "company": null,
      "status": "active",
      "billing_mode": "prepaid",
      "credit_balance": 25.0,
      "currency": "USD",
      "country_code": "GB",
      "currency_source": "default",
      "created_at": "2026-09-01T10:15:00"
    }
    

    The email is yours, which shows the token works. Update your profile describes each field.

When the access token runs out, every request answers 401 with Invalid or expired token. Exchange the refresh token for a new pair with POST /auth/refresh. It needs no password, no bot check and no second factor:

Shell
curl -s -X POST https://api.coritan.com/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg token "$CORITAN_REFRESH" '{refresh_token: $token}')" \
  > login.json
export CORITAN_TOKEN=$(jq -r .access_token login.json)
CORITAN_REFRESH=$(jq -r .refresh_token login.json)

The answer has the same fields as a sign-in. Keep the new refresh_token each time: it starts a full lifetime of its own. The one you sent works once. Sending it again more than a minute later signs the session out (Sessions). When a refresh token has expired too, sign in again as in section 4.

Run the GET /auth/me request from section 4 again. It answers with your account, using the new token.

401 Invalid or expired token with a ct_ key
The key is wrong, or it was revoked. Check that $CORITAN_TOKEN holds the whole key, or create a new one.
403 with "code": "api_key_read_only"
The key has only Read permissions. Create a key with a Write permission for a script that makes changes.
403 with "code": "api_key_route_refused"
No key can reach that route. Sign in as in section 4, or use the dashboard.
401 Invalid email or password
The email or the password is wrong. The answer is the same for both.
403 Account is suspended or closed
The account cannot sign in or use a key. Contact support.
401 That code is not right
The code is wrong, or you used it already: each code from the app works once. Wait for the next one.
401 Invalid or expired token from /auth/mfa/verify
The mfa_token is more than ten minutes old. Sign in again.
401 with "error": "mfa_required"
You sent the mfa_token to another endpoint. Finish section 4 first.
401 Token invalidated by password change
Someone changed the account's password after the token was issued. Sign in again with the new password.
403 Not authenticated
The request has no Authorization header. Check that $CORITAN_TOKEN is set in the shell you are using.
429 Too many authentication attempts. Please try again later.
Too many sign-in or refresh attempts failed from your address. Wait a minute, as the Retry-After header says.

API operations on this page

MethodPathWhat it does
GET/api/v1/auth/turnstileTurnstile config
POST/api/v1/auth/loginLogin
POST/api/v1/auth/mfa/verifySecond step of signing in
GET/api/v1/auth/meThe signed-in account
GET/api/v1/services/List services
POST/api/v1/auth/refreshA new token pair for a refresh token, which is then used up (app/domains/auth/sessions.py)