Tarsana docsDashboard

Run it on your provider

In this quickstart you store your Hetzner Cloud API token in Tarsana and create a server from your spec in your own Hetzner Cloud project.

Before you start, finish Build your first image. You need the shell variables from it: TENANT, ACCESS_TOKEN and SUBMISSION. Creating and removing servers needs the deployer role, which the account you signed up with has.

1. Create an API token at Hetzner

In the Hetzner Cloud Console, open the project the server should run in, go to Security, then API tokens, and create a token with Read & Write permission. Copy it.

export HCLOUD_TOKEN=paste-your-hetzner-api-token

2. Store it in Tarsana

Send the token to set-provider-credential. Tarsana stores it encrypted for your tenant only and never shows it again.

curl -X POST "https://api.tarsana.io/v1/tenants/$TENANT/providers/hetzner/credential" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT" \
  -H "Content-Type: application/json" \
  --data "{\"provider_token\": \"$HCLOUD_TOKEN\"}"

If it worked, you see its fingerprint:

{
  "credential_set": true,
  "fingerprint": "sha256:15dedf1c99544c41",
  "set_at": "2026-10-05T02:15:40.551914Z",
  "replaced": false
}

The fingerprint is sha256: followed by the first 16 hex digits of the SHA-256 of your token. Check it against your own copy:

printf %s "$HCLOUD_TOKEN" | sha256sum | cut -c1-16

3. Check where the server will run

The placement in your spec says where: provider is hetzner-cloud, server_type is a Hetzner server type, and region is optional. The spec from the first quickstart already has one:

"placement": {"provider": "hetzner-cloud", "server_type": "cpx12", "region": "nbg1"}

The placement is not part of the spec's hash, and Tarsana keeps the placement of the first submission of a spec. If you submitted a spec without a placement, adding one does not change the stored submission: change something else in the spec as well, so that it gets a new submission ID, and submit it. A placement Tarsana cannot use is refused at submission with spec_invalid, together with the providers you can use.

4. Create the server

Call create-server with a name of your own for this server in request_id:

curl -X POST "https://api.tarsana.io/v1/tenants/$TENANT/submissions/$SUBMISSION/create-server" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT" \
  -H "Content-Type: application/json" \
  --data '{"request_id": "web-1"}'

If it worked, you see the server's ID at Hetzner in created.target.id:

{
  "provider_name": "hetzner",
  "created": {
    "action_id": "ff97f81e3825703462ecab41c1c4ffa3",
    "idempotent_replay": false,
    "operation_id": "5000",
    "state": "provisioning",
    "target": {
      "id": "1000",
      "region": "fsn1"
    }
  },
  "first_boot": {
    "carrier": "user-data",
    "runs": "A sentence about the one-time setup.",
    "secret": "A sentence about the server's own enrollment token."
  }
}

A 200 answer means the server exists at Hetzner. It then boots, runs its one-time setup and connects to Tarsana. Password login is switched off and no SSH key is installed.

If the request fails part of the way, send the same request again with the same request_id: you get the same server back, never a second one. The errors you may see:

CodeWhat to do
create_unavailableCheck your stored token with provider-credential.
not_creatableThe stored spec has no placement. See step 3.
not_provisionableWait until the build has succeeded.
create_refusedHetzner refused, for example the server type is not sold in that region. Fix the cause and try again.

5. See the other servers in your account

servers lists the servers in your Hetzner project that Tarsana did not create, and counts the ones it did:

curl "https://api.tarsana.io/v1/tenants/$TENANT/providers/hetzner/servers" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT"
{
  "accounted_for": 1,
  "servers": [
    {
      "id": "1001",
      "name": "my-own-box",
      "server_type": "cx22",
      "location": "nbg1",
      "state": "running",
      "created": "2026-10-05T02:15:41+00:00",
      "addresses": [
        "198.51.100.11"
      ],
      "labels": {},
      "reason_code": "no_label",
      "reason": "A sentence that says why this server is listed."
    }
  ]
}

Your new server is counted in accounted_for. A server you made yourself is listed, with the reason Tarsana does not count it as its own. Tarsana never removes a listed server unless you ask.

6. Remove the server when you are done

decommission-server revokes the server's own credentials, deletes it at Hetzner and waits until it is gone:

export SERVER_ID=paste-the-created.target.id-value
curl -X POST "https://api.tarsana.io/v1/tenants/$TENANT/providers/hetzner/servers/$SERVER_ID/decommission" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT"
{
  "decommissioned": {
    "state": "destroyed",
    "observed": "not-found",
    "action_id": "ff97f81e3825703462ecab41c1c4ffa3",
    "idempotent_replay": false,
    "requested_at": "2026-10-05T02:15:41.166635+00:00",
    "completed_at": "2026-10-05T02:15:41.169045+00:00",
    "target": {
      "id": "1000",
      "region": "fsn1"
    }
  }
}

You cannot undo this. If it stops part of the way, send the same request again to continue.

Next steps

View this page as Markdown