> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Create an account, issue a set of API credentials, and make your first signed request.

This walks you from nothing to an authenticated API call. It should take a few
minutes.

## Prerequisites

* A way to receive email, to verify the account.
* `curl` and Python 3, for the signing example at the end.

## Create an account

<Steps>
  <Step title="Sign up">
    Register at [console.basaltic.sh](https://console.basaltic.sh/auth/signup).

    Signing up, verifying your email and setting up billing are console flows —
    they are not part of the public API.
  </Step>

  <Step title="Verify your email">
    Registration sends a six-digit code. The account cannot create resources
    until it is verified.
  </Step>
</Steps>

## Issue API credentials

Your console login signs you in as a person. Programmatic access uses a
**service account** and its access key instead. Create one in the console under
**IAM → Service accounts**, or over the API:

<Steps>
  <Step title="Create the service account">
    ```bash theme={null}
    POST https://iam.basaltic.sh/v1/service-accounts
    {"name": "deploy-bot"}
    ```

    A service account has no permissions of its own. Attach a policy, or add it
    to a group that carries one, before it can do anything.
  </Step>

  <Step title="Create a credential on it">
    ```bash theme={null}
    POST https://iam.basaltic.sh/v1/service-accounts/{service_account_id}/credentials
    {"name": "production-key"}
    ```

    The response carries the credential and its secret:

    ```json theme={null}
    {
      "credential": {
        "id": "3f8a1c2d-4b5e-6789-abcd-ef0123456789",
        "name": "production-key",
        "access_key_id": "BYCLD550E8400E29B41D4",
        "created_at": "2026-01-15T09:30:00Z"
      },
      "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    }
    ```

    <Warning>
      `secret_access_key` is returned **once**, at creation. It is not stored in
      a form the API can show you again. If you lose it, delete the credential
      and create another.
    </Warning>
  </Step>
</Steps>

## Make a signed request

Every programmatic request is signed with `BASALTIC-HMAC-SHA256` — an HMAC over
a canonical form of the request, keyed by a value derived from your secret. The
signature covers the method, path, query, three headers, and a hash of the body,
so nothing in the request can be altered in flight.

The script below signs a request and calls it. It is a complete, working
implementation of the scheme.

```python signed_request.py theme={null}
import hashlib, hmac, os, secrets, urllib.parse
from datetime import datetime, timezone

import requests

ACCESS_KEY = os.environ["BASALTIC_ACCESS_KEY_ID"]
SECRET_KEY = os.environ["BASALTIC_SECRET_ACCESS_KEY"]
REGION     = "sa-saopaulo-1"
HOST       = f"compute.{REGION}.basaltic.sh"


def _hmac(key: bytes, msg: str) -> bytes:
    return hmac.new(key, msg.encode(), hashlib.sha256).digest()


def signed_headers(method, path, query="", body=b""):
    now = datetime.now(timezone.utc)
    x_date = now.strftime("%Y%m%dT%H%M%SZ")
    date = x_date[:8]
    nonce = secrets.token_hex(16)

    body_hash = hashlib.sha256(body).hexdigest() if body else "UNSIGNED-PAYLOAD"

    # Exactly these three headers are signed (plus x-amz-security-token when
    # you are using temporary STS credentials).
    headers = {"host": HOST, "x-date": x_date, "x-nonce": nonce}
    names = sorted(headers)

    pairs = sorted(urllib.parse.parse_qsl(query, keep_blank_values=True))
    canonical_query = "&".join(
        f"{urllib.parse.quote(k, safe='-_.~')}={urllib.parse.quote(v, safe='-_.~')}"
        for k, v in pairs
    )

    canonical_request = "\n".join([
        method,
        path,
        canonical_query,
        "".join(f"{n}:{headers[n].strip()}\n" for n in names),
        ";".join(names),
        body_hash,
    ])

    # The timestamp line is the credential date at MIDNIGHT, not x-date.
    string_to_sign = "\n".join([
        "BASALTIC-HMAC-SHA256",
        f"{date}T000000Z",
        f"{date}/{REGION}/basaltic/basaltic_request",
        hashlib.sha256(canonical_request.encode()).hexdigest(),
    ])

    key = _hmac(("BASALTIC" + SECRET_KEY).encode(), date)
    for part in (REGION, "basaltic", "basaltic_request"):
        key = _hmac(key, part)
    signature = hmac.new(key, string_to_sign.encode(), hashlib.sha256).hexdigest()

    out = {
        "Host": HOST,
        "X-Date": x_date,
        "X-Nonce": nonce,
        "Authorization": (
            f"BASALTIC-HMAC-SHA256 Credential={ACCESS_KEY}/{date}/{REGION}"
            f"/basaltic/basaltic_request, "
            f"SignedHeaders={';'.join(names)}, Signature={signature}"
        ),
    }
    if body:
        out["X-Content-Sha256"] = body_hash
    return out


if __name__ == "__main__":
    path = "/v1/instances"
    r = requests.get(
        f"https://{HOST}{path}",
        headers=signed_headers("GET", path),
        timeout=30,
    )
    print(r.status_code, r.text)
```

Run it with your credentials in the environment:

```bash theme={null}
export BASALTIC_ACCESS_KEY_ID=BYCLD550E8400E29B41D4
export BASALTIC_SECRET_ACCESS_KEY=...
python signed_request.py
```

A fresh account has no instances, so a healthy response is `200` with an empty
list — that is the call succeeding, not failing.

<Tip>
  Signing by hand is only worth it when you are writing a client. For everyday
  use the [CLI](/cli) signs for you.
</Tip>

## Where to go next

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/authentication">
    The full signing rules — clock skew, replay protection, streaming bodies,
    and temporary credentials.
  </Card>

  <Card title="Regions and endpoints" icon="globe" href="/regions">
    Which host each service answers on.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    Every operation, with schemas.
  </Card>

  <Card title="Support" icon="life-ring" href="/support">
    When something is wrong and you need a person.
  </Card>
</CardGroup>
