Skip to content

Developers

Build on the network

Run jobs on the worldwide network, follow their progress and collect verified results from your own code — or build an app that does it for other people. One HTTPS API for your own scripts and for apps that act for other people. An API key (dk_…) for your code, an OAuth 2 access token (do_…) for an app someone allowed, scopes that limit both, and webhooks signed with HMAC so you hear about events as they happen.

The credentials

  • dk_…API key — your scripts and servers
  • do_…Access token — one hour
  • dr_…Refresh token — thirty days, one use
  • whsec_…Webhook signing secret

Every one is sent as Authorization: Bearer …, except the webhook secret, which never leaves your server.

Getting started

Two ways in

Use a key when the code is yours and acts for your own account. Use OAuth when people sign in to your app and let it act for them.

With an API key

  1. 1

    Create a key in developer settings. Give it a name and only the scopes it needs, and an expiry if it is for something temporary.

    Until your e-mail address is confirmed, a key can only carry the read scopes.

  2. 2

    Copy the key when it is shown. It appears once: only a fingerprint of it is kept, so a lost key cannot be recovered, only replaced.

  3. 3

    Send it as a bearer token on every request:

    shell
    curl https://api.thecubefloor.com/v1/me \
      -H "Authorization: Bearer dk_…"
  4. 4

    Revoke it from the same page the moment it is no longer needed. The list shows when each key was last used.

With OAuth 2

The authorization code grant, with PKCE (S256) required on every request. Every app is a confidential client with a secret.

  1. 1

    Register the app in developer settings: its name (people see it when they are asked), its redirect URIs and the scopes it may ask for. You get a client_id and a client_secret, the secret shown once. Registering an app needs a confirmed e-mail address.

    Redirect URIs must match character for character. They are absolute https:// addresses without a fragment; plain http:// is accepted only to localhost or a loopback address, for development and native apps.

  2. 2

    Make a random code_verifier and send the person’s browser to /oauth/authorize with its SHA-256 as code_challenge:

    shell
    # A verifier: 43 to 128 unreserved characters. Keep it until the exchange.
    CODE_VERIFIER=$(openssl rand -base64 64 | tr -dc 'A-Za-z0-9' | cut -c1-64)
    CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary \
      | openssl base64 -A | tr '+/' '-_' | tr -d '=')
    
    https://api.thecubefloor.com/oauth/authorize?response_type=code
      &client_id=app_…
      &redirect_uri=https%3A%2F%2Fyour.app%2Fcallback
      &scope=account%3Aread%20jobs%3Aread
      &state=…
      &code_challenge=$CODE_CHALLENGE
      &code_challenge_method=S256
  3. 3

    They see our consent page on thecubefloor.com, with your app’s name, what it asks for and where it will return. If they approve, the browser comes back to your redirect URI with code and your state; if they decline, with error=access_denied.

  4. 4

    Trade the code, within a minute and once only, for tokens. Authenticate with HTTP Basic or with client_id and client_secret in the form:

    shell
    curl https://api.thecubefloor.com/oauth/token \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -d grant_type=authorization_code \
      -d code="$CODE" \
      -d redirect_uri=https://your.app/callback \
      -d code_verifier="$CODE_VERIFIER"
  5. 5

    The answer holds an access token (do_…, good for an hour) and a refresh token (dr_…, thirty days). Each refresh spends the refresh token and returns a new pair; presenting a spent one ends every token from the same approval, since one of its two holders is a thief.

    json · shell
    {
      "access_token": "do_…",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "dr_…",
      "scope": "account:read jobs:read"
    }
    
    curl https://api.thecubefloor.com/oauth/token \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -d grant_type=refresh_token \
      -d refresh_token="$REFRESH_TOKEN"
  6. 6

    When the person disconnects in your app, give the token back (RFC 7009). A refresh token ends the whole approval:

    shell
    curl https://api.thecubefloor.com/oauth/revoke \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -d token="$REFRESH_TOKEN"

When something is refused

Errors are {"error": "E_…", "message": "…"}: a code your program can branch on and a sentence a person can act on. 401 means the credential is missing, unknown, revoked or expired; 403 E_SCOPE that it lacks the scope; 403 E_SESSION_ONLY that the operation is for the website alone. The /oauth/ endpoints answer in RFC 6749’s shape instead.

Scopes

What a key or token may do

A key or token can never do more than its account could itself, and an account whose e-mail address is not confirmed can only hand out the read scopes. Everything not listed here — top-ups, withdrawals, claiming machines, connecting storage, account settings, and managing keys and apps — stays with the website.

The scopes and the endpoints each one opens
ScopeWhat it allowsEndpoints
account:read

Read your account

Your account details and balances.

  • GET /v1/me
jobs:read

See your jobs

The jobs you have run, their progress and their results.

  • GET /v1/jobs
  • GET /v1/jobs/{id}
jobs:write

Run and cancel jobs

Start, estimate and cancel jobs, spending your account’s credit.

  • POST /v1/jobs
  • POST /v1/jobs/estimate
  • POST /v1/jobs/{id}/cancel
machines:read

See your machines

Your machines, the commands sent to them and what they have earned.

  • GET /v1/me/machines
  • GET /v1/me/earnings
  • GET /v1/me/machines/{handle}/control
machines:write

Control your machines

Restart, shut down or re-address your machines.

  • POST /v1/me/machines/{handle}/control
money:read

See your top-ups and withdrawals

Your top-ups and withdrawals, and where each one stands.

  • GET /v1/me/topups
  • GET /v1/me/withdrawals
drive:read

See your storage connection

Whether your own storage is connected, and a summary of it.

  • GET /v1/drive/google
  • GET /v1/storage/mine
webhooks:write

Manage your webhooks

Add, test and delete the addresses your events are sent to.

  • /v1/me/webhooks …

Webhooks

Hear about it as it happens

Register an https:// address and the events you want in developer settings. Each event is POSTed there as JSON, signed with the secret shown when you created the webhook.

Events

Event types and what each means
EventSent when
job.finishedA job finished and its results are ready
job.failedA job failed
job.undeliveredA job ran but its results could not be delivered
topup.confirmedA top-up was confirmed
withdrawal.paidA withdrawal was paid
withdrawal.rejectedA withdrawal was rejected
machine.claimedA machine’s earnings now come to this account
pingA test event, sent when you press “Send test”

Headers

DAIN-Webhook-Id
The event’s id, unchanged on every retry. Use it to drop a repeat: a delivery is made at least once.
DAIN-Webhook-Timestamp
Unix seconds of this attempt. Refuse anything more than five minutes old.
DAIN-Webhook-Signature
HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your whsec_… secret, in hex.
v1=<hex HMAC-SHA256(secret, timestamp + "." + body)>

What arrives

http
POST /your/webhook HTTP/1.1
Content-Type: application/json
DAIN-Webhook-Id: evt_…
DAIN-Webhook-Timestamp: 1793351642
DAIN-Webhook-Signature: v1=5f2b…c09e

{
  "id": "evt_…",
  "type": "job.finished",
  "created_at": "2026-10-30T09:14:02Z",
  "data": { "job": { "id": "job_…", "state": "done", … } }
}

data holds the object as the API shows it on its own endpoint: the job, the top-up, the withdrawal (its destination masked) or the machine. A cancelled job is your own doing and is not reported.

Retries

  1. Attempt 1At once
  2. Attempt 2+1m
  3. Attempt 3+5m
  4. Attempt 4+30m
  5. Attempt 5+2h
  6. Attempt 6+6h
  7. Attempt 7+24h
  8. Disabled

Any 2xx ends it. Otherwise the delivery is tried again after each of these waits — seven attempts over about 33 hours — and when the last one fails the webhook is disabled. Sending a test that gets a 2xx enables it again. Redirects are not followed.

Addresses are checked after DNS resolution, at registration and at every delivery: nothing private, loopback, link-local or reserved is ever called.

Verify the signature

Compute the signature over the body exactly as it arrived — before any JSON parsing — and compare in constant time.

python
import hashlib, hmac, time

def verify(secret: str, headers, body: bytes) -> bool:
    """body: the request body exactly as it arrived, before json.loads."""
    ts = headers["DAIN-Webhook-Timestamp"]
    sig = headers.get("DAIN-Webhook-Signature", "")
    if abs(time.time() - int(ts)) > 300:  # more than five minutes old
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256)
    return hmac.compare_digest(sig, "v1=" + mac.hexdigest())

API reference

Every endpoint

Read live from /v1/openapi.yaml, the OpenAPI 3.1 contract the server is tested against. Point a client generator at the same address.