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 serversdo_…Access token — one hourdr_…Refresh token — thirty days, one usewhsec_…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
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
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
Send it as a bearer token on every request:
shellcurl https://api.thecubefloor.com/v1/me \ -H "Authorization: Bearer dk_…" - 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
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_idand aclient_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; plainhttp://is accepted only tolocalhostor a loopback address, for development and native apps. - 2
Make a random
code_verifierand send the person’s browser to/oauth/authorizewith its SHA-256 ascode_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
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
codeand yourstate; if they decline, witherror=access_denied. - 4
Trade the code, within a minute and once only, for tokens. Authenticate with HTTP Basic or with
client_idandclient_secretin the form:shellcurl 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
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
When the person disconnects in your app, give the token back (RFC 7009). A refresh token ends the whole approval:
shellcurl 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.
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 | Sent when |
|---|---|
job.finished | A job finished and its results are ready |
job.failed | A job failed |
job.undelivered | A job ran but its results could not be delivered |
topup.confirmed | A top-up was confirmed |
withdrawal.paid | A withdrawal was paid |
withdrawal.rejected | A withdrawal was rejected |
machine.claimed | A machine’s earnings now come to this account |
ping | A 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
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
- Attempt 1At once
- Attempt 2+1m
- Attempt 3+5m
- Attempt 4+30m
- Attempt 5+2h
- Attempt 6+6h
- Attempt 7+24h
- 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.
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.