Developer reference
API documentation
Build a cost-per-mile calculation into your server application.
Create an account · Sign in · Verify email · API keys · Dashboard Usage
Current pricing · Current API catalog · OpenAPI · JSON Schema
Quickstart
Create an account, verify your email, create an ordinary API key, then run one example from your own server environment. This page only displays and copies text.
Illustrative example price: $0.005000 per new successful operation. Free allowance: $1.500000 per organization per UTC calendar month after verification, used first and never carried forward. Another key does not create another allowance. See /pricing and the API catalog for the current server price. This reference is not your balance or a promise that free funds remain.
Supply DOFT_API_KEY through your server secret environment. Never put it in browser code, a URL, cookies, logs or a shell-history literal. No test/live key modes exist.
Create a UUID v4 once for each new operation and export it as DOFT_IDEMPOTENCY_KEY. Save that value and the complete input for any deliberate manual retry within the replay window. Do not generate another UUID each time you rerun an example.
Examples require curl 7.76+ with --fail-with-body and Python 3.10+ for JSON validation, Node.js 24 native fetch, or Python 3.10+ standard library urllib. Each makes at most one request with a 20-second timeout, refuses redirects and handles HTTP errors separately from transport errors. They do not retry automatically.
API references: GET /api/v1/tools (catalogue, versions and priceUsd), GET /api/v1/tools/cost-per-mile/schema (schema), GET /api/v1/health (minimal health), GET /api/v1/usage (current-key aggregates, at most 31 UTC days). GET /api/v1/openapi.json provides the API specification.
Authentication
Send Authorization: Bearer followed by your ordinary API key. A key is dt_ followed by 43 base64url characters. The key is shown once at creation; store it as a server secret. API cookies are ignored and do not authenticate requests.
Every request checks the current key, expiry or revocation, organization status, tool scope and any allowed IPv4/IPv6 CIDR restriction. Without an expiry a key remains valid until revoked. Keys do not issue browser sessions. There is no test/live mode.
Rotate by creating a new key and then revoking the old one. Keys in the same organization share balance, allowance, spending limits and replay scope. A new tool never silently expands an existing scope. Revocation or account suspension also denies replay.
An owner can set 1–60 attempts per minute per key; the default is 60. The organization default is 120 attempts per minute and may be configured by the service. Attempts include validation failures and replay. Rate limits are separate from paid spending caps and successful operation charges; use the returned headers for the applicable bucket.
Default paid spending caps are $10.000000 per UTC day and $100.000000 per UTC month. Free usage and top-ups do not count as paid consumption. Manage purchases and optional automatic top-up in Billing when available; this page initiates no payment.
Cost per mile
POST JSON only: Content-Type: application/json; no compression; body at most 8192 bytes. Idempotency-Key is required: 16–128 ASCII letters, digits, underscore or hyphen; UUID v4 is recommended.
All costs are monthly integer cents, not dollars. Omitted costs default to 0. milesPerMonth is required. Reject unknown fields, duplicate JSON keys, null, strings and fractions.
The synthetic input below totals $10,050.00 for 9,500 miles. The same server formula and schema are used by the working API.
data contains integer cents and JSON-number USD per mile rounded to four decimal places. meta.operationCostUsd and meta.chargedUsd are exact USD strings with six decimal places; formula version 1.0.0 is separate from API v1.
| Field | Meaning / group | Unit | Required / default | Inclusive range |
|---|---|---|---|---|
truckPaymentCents | Truck payment fixed | integer cents/month | Optional; 0 | 0–10000000 |
trailerPaymentCents | Trailer payment fixed | integer cents/month | Optional; 0 | 0–10000000 |
insuranceCents | Insurance fixed | integer cents/month | Optional; 0 | 0–10000000 |
permitsAndPlatesCents | Permits and plates fixed | integer cents/month | Optional; 0 | 0–10000000 |
otherFixedCents | Other fixed costs fixed | integer cents/month | Optional; 0 | 0–10000000 |
fuelCents | Fuel variable | integer cents/month | Optional; 0 | 0–10000000 |
maintenanceCents | Maintenance variable | integer cents/month | Optional; 0 | 0–10000000 |
tiresCents | Tires variable | integer cents/month | Optional; 0 | 0–10000000 |
tollsCents | Tolls variable | integer cents/month | Optional; 0 | 0–10000000 |
driverPayCents | Driver pay variable | integer cents/month | Optional; 0 | 0–10000000 |
otherVariableCents | Other variable costs variable | integer cents/month | Optional; 0 | 0–10000000 |
milesPerMonth | Miles driven per month mileage | miles/month | Required; no default | 1–50000 |
#!/bin/sh
# curl 7.76+ and Python 3.10+. Set server environment securely first.
set -eu
if ! printf '%s' "${DOFT_API_KEY-}" | python3 -c 'import re,sys; sys.exit(0 if re.fullmatch(r"dt_[A-Za-z0-9_-]{43}",sys.stdin.read()) else 1)'; then
echo 'Set a valid DOFT_API_KEY in the server environment.' >&2; exit 1
fi
if ! printf '%s' "${DOFT_IDEMPOTENCY_KEY-}" | python3 -c 'import re,sys; sys.exit(0 if re.fullmatch(r"[A-Za-z0-9_-]{16,128}",sys.stdin.read()) else 1)'; then
echo 'Set the saved DOFT_IDEMPOTENCY_KEY for this operation.' >&2; exit 1
fi
response=$(mktemp)
trap 'rm -f "$response"' EXIT HUP INT TERM
if status=$(curl --disable --silent --show-error --fail-with-body --max-time 20 --max-redirs 0 \
--request POST 'https://api.dofttools.com/api/v1/tools/cost-per-mile' \
--header "Authorization: Bearer $DOFT_API_KEY" \
--header 'Content-Type: application/json' \
--header "Idempotency-Key: $DOFT_IDEMPOTENCY_KEY" \
--data-raw '{"truckPaymentCents":240000,"insuranceCents":115000,"fuelCents":570000,"maintenanceCents":80000,"milesPerMonth":9500}' \
--output "$response" --write-out '%{http_code}'); then
case "$status" in 2??) ;; *) echo "HTTP $status. No automatic retry." >&2; exit 1 ;; esac
else
case "$status" in [345]??) echo "HTTP $status. No automatic retry." >&2 ;;
*) echo 'Transport failure; outcome unknown. Preserve operation context.' >&2 ;; esac
exit 1
fi
python3 - "$response" <<'PY'
import json, sys
try:
with open(sys.argv[1], encoding='utf-8') as response:
data = json.load(response)
except (OSError, ValueError):
sys.exit('Non-JSON response; outcome unknown. Preserve operation context.')
print(json.dumps(data, indent=2))
PY// Node.js 24; save as request.mjs. Supply server environment securely.
const apiKey = process.env.DOFT_API_KEY ?? '';
const operationKey = process.env.DOFT_IDEMPOTENCY_KEY ?? '';
if (apiKey.length !== 46 || !/^dt_[A-Za-z0-9_-]{43}$/.test(apiKey) || /[^A-Za-z0-9_-]/.test(operationKey) || !/^[A-Za-z0-9_-]{16,128}$/.test(operationKey)) {
console.error('Set valid DOFT_API_KEY and saved DOFT_IDEMPOTENCY_KEY.');
process.exit(1);
}
try {
const response = await fetch('https://api.dofttools.com/api/v1/tools/cost-per-mile', {
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(20_000),
headers: { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json', 'Idempotency-Key': operationKey },
body: '{"truckPaymentCents":240000,"insuranceCents":115000,"fuelCents":570000,"maintenanceCents":80000,"milesPerMonth":9500}',
});
if (!response.ok) {
console.error('HTTP ' + response.status + '. No automatic retry.');
process.exitCode = 1;
} else {
try { console.log(JSON.stringify(await response.json(), null, 2)); }
catch { console.error('Non-JSON response; outcome unknown. Preserve operation context.'); process.exitCode = 1; }
}
} catch {
console.error('Transport failure; outcome unknown. Preserve operation context.');
process.exitCode = 1;
}# Python 3.10+; standard library only. Supply server environment securely.
import json, os, re, sys
import http.client
import urllib.request
import urllib.error
api_key = os.environ.get('DOFT_API_KEY', '')
operation_key = os.environ.get('DOFT_IDEMPOTENCY_KEY', '')
if not re.fullmatch(r'dt_[A-Za-z0-9_-]{43}', api_key) or not re.fullmatch(r'[A-Za-z0-9_-]{16,128}', operation_key):
sys.exit('Set valid DOFT_API_KEY and saved DOFT_IDEMPOTENCY_KEY.')
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
request = urllib.request.Request(
'https://api.dofttools.com/api/v1/tools/cost-per-mile', method='POST',
headers={'Authorization': 'Bearer ' + api_key, 'Content-Type': 'application/json', 'Idempotency-Key': operation_key},
data=b'{"truckPaymentCents":240000,"insuranceCents":115000,"fuelCents":570000,"maintenanceCents":80000,"milesPerMonth":9500}',
)
try:
with urllib.request.build_opener(NoRedirect()).open(request, timeout=20) as response:
if not 200 <= response.status < 300:
sys.exit('HTTP ' + str(response.status) + '. No automatic retry.')
try:
data = json.load(response)
except (ValueError, UnicodeError):
sys.exit('Non-JSON response; outcome unknown. Preserve operation context.')
print(json.dumps(data, indent=2))
except urllib.error.HTTPError as error:
sys.exit('HTTP ' + str(error.code) + '. No automatic retry.')
except (urllib.error.URLError, OSError, http.client.HTTPException):
sys.exit('Transport failure; outcome unknown. Preserve operation context.')Synthetic success response
{
"data": {
"fixedCostCents": 355000,
"variableCostCents": 650000,
"totalCostCents": 1005000,
"costPerMileUsd": 1.0579,
"fixedPerMileUsd": 0.3737,
"variablePerMileUsd": 0.6842,
"breakdown": [
{
"field": "truckPaymentCents",
"group": "fixed",
"amountCents": 240000,
"costPerMileUsd": 0.2526
},
{
"field": "insuranceCents",
"group": "fixed",
"amountCents": 115000,
"costPerMileUsd": 0.1211
},
{
"field": "fuelCents",
"group": "variable",
"amountCents": 570000,
"costPerMileUsd": 0.6
},
{
"field": "maintenanceCents",
"group": "variable",
"amountCents": 80000,
"costPerMileUsd": 0.0842
}
]
},
"meta": {
"tool": "cost-per-mile",
"version": "1.0.0",
"operationId": "73e0691d-eb7e-4563-9da0-7616f04a69da",
"operationCostUsd": "0.005000",
"chargedUsd": "0.005000",
"replayed": false
}
}Read Usage
GET /api/v1/usage requires an ordinary authorized API key. It returns UTC daily aggregates for the current original key only, not other keys in the organization. Use dashboard Usage for organization-wide history.
Supply both from and to as YYYY-MM-DD, or omit both for the current UTC calendar month. from is inclusive and to exclusive; the interval must be positive and at most 31 UTC days. The example omits dates to read the current month.
count is the number of original successful API operations; costUsd is their original total cost, equal to freeUsedUsd + paidUsedUsd. Replays, failed attempts and WEB calculations add no usage rows.
correctedFreeUsd and correctedPaidUsd show corrections separately from original consumption. They are not new calculations and do not mean an automatic refund to a card. All USD amounts are exact strings with six decimal places. Days without operations are omitted.
The synthetic response below illustrates three operations at $0.005000 each, funded from free allowance. It is not your current price or balance. The current-key totals can differ from dashboard totals across all keys.
#!/bin/sh
# curl 7.76+ and Python 3.10+. Set server environment securely first.
set -eu
if ! printf '%s' "${DOFT_API_KEY-}" | python3 -c 'import re,sys; sys.exit(0 if re.fullmatch(r"dt_[A-Za-z0-9_-]{43}",sys.stdin.read()) else 1)'; then
echo 'Set a valid DOFT_API_KEY in the server environment.' >&2; exit 1
fi
response=$(mktemp)
trap 'rm -f "$response"' EXIT HUP INT TERM
if status=$(curl --disable --silent --show-error --fail-with-body --max-time 20 --max-redirs 0 \
--request GET 'https://api.dofttools.com/api/v1/usage' \
--header "Authorization: Bearer $DOFT_API_KEY" \
--output "$response" --write-out '%{http_code}'); then
case "$status" in 2??) ;; *) echo "HTTP $status. No automatic retry." >&2; exit 1 ;; esac
else
case "$status" in [345]??) echo "HTTP $status. No automatic retry." >&2 ;;
*) echo 'Transport failure; outcome unknown. Preserve operation context.' >&2 ;; esac
exit 1
fi
python3 - "$response" <<'PY'
import json, sys
try:
with open(sys.argv[1], encoding='utf-8') as response:
data = json.load(response)
except (OSError, ValueError):
sys.exit('Non-JSON response; outcome unknown. Preserve operation context.')
print(json.dumps(data, indent=2))
PYSynthetic Usage response
{
"from": "2026-10-01",
"to": "2026-11-01",
"daily": [
{
"day": "2026-10-23",
"tool": "cost-per-mile",
"count": 3,
"costUsd": "0.015000",
"freeUsedUsd": "0.015000",
"paidUsedUsd": "0.000000",
"correctedFreeUsd": "0.000000",
"correctedPaidUsd": "0.000000"
}
]
}Errors and safe retries
Replay lasts 24 hours from operation_at of the successful transaction, within organization + tool + Idempotency-Key. Send the same complete input and a currently valid authorized key. A different input gives 409 idempotency_conflict; an operation still running gives 409 request_in_progress and Retry-After.
Replay preserves data, version, operationId and operationCostUsd; replayed becomes true and chargedUsd is "0.000000". X-Request-Id identifies each HTTP attempt, not the operation. Replay does not debit again, but attempts still count toward rate limits.
A timeout, broken connection, non-JSON or incomplete response before or after commit means the outcome is unknown. It does not prove that no charge occurred and does not promise an automatic refund. Keep the original key and input; inspect the outcome before a deliberate retry within 24 hours. Never use a blind automatic retry loop.
After 24 hours or loss of operation context, do not automatically retry: the same idempotency key may create a new paid operation. Revoked keys and blocked accounts cannot bypass restrictions through replay.
429 rate_limited is an attempt-frequency limit; 429 spend_limit_reached is a paid UTC spending cap. Both and 409 request_in_progress include Retry-After in integer seconds. X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix seconds) and X-RateLimit-Scope (key, organization or ip) describe the selected rate bucket when checked; early errors may omit them. Waiting does not guarantee success. Decide explicitly whether the original operation remains safe to retry.
X-Balance-Remaining-Usd is the available free + paid balance as a six-decimal USD string. An absent header means unknown or not applicable, never zero. Application API errors contain only {"error":{"code":"..."}}; no message or details fields are promised. Network, proxy and incomplete responses may have no JSON body or request ID.
The API is for server-to-server requests, with no third-party browser CORS. Omit Origin in ordinary server requests: if supplied it must equal the API origin, otherwise 403 origin_denied. Browser session, invalid_session, unsupported_auth_method for Bearer on WEB, CSRF, Turnstile challenge and web_quota_exceeded belong to WEB, not Bearer API prerequisites.
| HTTP | Code | Safe action |
|---|---|---|
| 400 | invalid_request, idempotency_key_required, invalid_idempotency_key | Correct the payload or required idempotency header before making a deliberate request. |
| 401 | invalid_key | Check the server secret, expiry and revocation; never expose the key while troubleshooting. |
| 402 | insufficient_balance | Review the available free and paid balance; a new operation needs sufficient funds. |
| 403 | scope_denied, ip_not_allowed, account_unavailable, origin_denied | Review tool scope, allowed server IP and organization restrictions. Do not bypass denial with replay. |
| 404 | tool_not_found, not_found | Check the host, active tool and documented route. |
| 405 | method_not_allowed | Use the supported method; inspect Allow. GET and HEAD never calculate. |
| 408 | request_timeout | The application stopped reading the request body after 5 seconds. Preserve operation context and assess a deliberate retry. |
| 409 | idempotency_conflict | Do not change input under an existing operation key. Check the original operation context. |
| 409 | request_in_progress | Wait for Retry-After, then explicitly assess a same-input retry within the replay window. |
| 413 | payload_too_large | Keep the JSON body within 8192 bytes. |
| 415 | unsupported_media_type | Use application/json without Content-Encoding compression. |
| 429 | rate_limited | Check the indicated attempt-rate bucket and Retry-After. Waiting does not guarantee success; no automatic retry. |
| 429 | spend_limit_reached | Review the paid UTC spending cap and Retry-After. This is separate from attempt-rate limits; no automatic retry. |
| 503 | service_unavailable | Preserve operation context and assess availability before a safe manual retry; never assume no charge. |