API
API reference
Every route a key can call: its method and path, the operation the key needs, what to send, and what comes back.
Overview
- Base URL
https://dashboard.attackdesk.com. SendAuthorization: Bearer atk_…with every request, and JSON bodies. See Getting started. - Each route needs the operation shown on the key, allowed to the key's person. See API keys.
- Rate limits, spending limits and what 402 and 429 mean are on Limits.
- Shapes are shortened:
…stands for values and fields left out. Times are ISO 8601 in UTC. ABoxand aPublishare shown under their routes.
People
Who is on the key's company, and who's online.
/api/v1/peopleEveryone on the company: name, email, role, whether they can use the app, and where they're active now.
Operation team.people.read
Returns
{
"people": [{
"id": "…", "name": "…", "email": "…",
"role": "developer", // owner | admin | developer | manager | user
"self": false,
"canUseApp": true,
"permissions": ["…"], // only when the key's person has the Team permission
"online": true, // active in the last onlineSeconds
"onlineIn": ["dashboard"], // app | dashboard | launcher
"lastSeenAt": "…", "lastSeenIn": "dashboard",
"app": { "role": "…", "grant": [], "deny": [] } // null for owners and admins
}],
"invited": [{ "email": "…", "role": "…", "expiresAt": "…" }], // only with the Team permission
"onlineSeconds": 180,
"at": "…"
}- If the key's person can't See the team, the list has only them.
- Online people come first, then by name.
Keys
Keys are made, changed, paused and deleted only on the dashboard's API Keys page. No route manages keys with a key. See API keys.
Code (git)
Your company's repository is on the code server. Copy its address from the dashboard's Code page; the username (your company's name) is already in it. The password is an API key for this company.
git clone https://<company>@code.attackdesk.com/<company>.gitClone, fetch and pull with git over HTTPS.
Operation code.pull
- The key's person needs Download and pull the code, and the company needs a verified card (a temporary $49 hold that's canceled, as for downloads).
- The company app's shared keys are refused.
- Each clone, fetch or pull counts as one API call, and stops when the company's or the person's general spending limit is used up.
git pushPush branches with git. The key also needs code.pull.
Operation code.push
- The key's person needs Push and upload code.
- A push that adds a secret is refused, and any AttackDesk key in it is switched off.
- A push that changes the protected core (src/sealed/) is refused unless it matches an official release.
- Each push counts as one API call.
AI workers (boxes)
A box is a Linux machine with your repository at /workspace/repo. It works as the key's person: use a person's own key. Only the box's person can work in it (commands, files, processes, previews, push); owners and admins can see, stop and delete anyone's. Every box request counts as an API call and against the 120-a-minute limit. Work in a box that is still starting or has stopped answers 409 box_starting or box_stopped. See AI workers for prices.
/api/v1/sandboxesYour boxes, newest first (up to 50). Stopped ones stay in the list until deleted.
Operation sandbox.read
Query
| Field | Type | |
|---|---|---|
all | 1 | Optional. Everyone's boxes, for owners and admins |
Returns
{ "boxes": [ Box ] }/api/v1/sandboxesStart a box on a branch (made from the default branch if it's new). The key also needs sandbox.read. The first minute is charged now.
Operation sandbox.start
Body
| Field | Type | |
|---|---|---|
size | string | small, medium, large or xlarge. Default medium |
branch | string | Default main |
maxMinutes | integer | 1 to 720. Default 60 |
budget | number | null | Dollars, 0.01 to 500. Default none |
idleMinutes | integer | 5 to 60. Default 15 |
label | string | null | Up to 80 characters |
Returns
201 Box- The key's person needs Create test environments, Spend on environments and Download and pull the code (403
not_allowed). - Too many boxes running: 429
too_many_boxes. No repository yet: 502clone_failed. A box that can't start isn't charged (503box_unavailable).
/api/v1/sandboxes/:idOne box: status, caps, what it has cost, preview links.
Operation sandbox.read
Returns
{
"id": "m…",
"size": "medium",
"branch": "main",
"label": null,
"status": "running", // starting | running | stopped | failed
"stopReason": null, // idle, time_cap, budget, spending_limit, funds, key_changed, stopped…
"workspace": "/workspace/repo",
"createdAt": "2026-09-27T17:00:00.000Z",
"stopsBy": "2026-09-27T18:00:00.000Z",
"stoppedAt": null,
"maxMinutes": 60,
"idleMinutes": 15,
"budget": null, // dollars
"previews": [{ "port": 5173, "url": "https://…attackdesk.app" }],
"spent": { "minutes": 1, "dollars": 0.003 }
}/api/v1/sandboxes/:id/eventsEverything done in the box, newest first (up to 200), with the key that did it.
Operation sandbox.read
Returns
{ "events": [{ "at": "…", "action": "exec", "detail": "npm test", "result": "exit 0", "apiKeyId": "…" }] }/api/v1/sandboxes/:id/execRun a shell command and wait for it.
Operation sandbox.exec
Body
| Field | Type | |
|---|---|---|
command | string | Up to 10,000 characters |
cwd | string | Optional. Default /workspace/repo |
timeoutMs | integer | Optional. 1,000 to 600,000. Default 120,000 |
Returns
{ "exitCode": 0, "success": true, "stdout": "…", "stderr": "" }- stdout and stderr are cut at 200,000 characters.
/api/v1/sandboxes/:id/processesStart a command in the background, such as a dev server.
Operation sandbox.exec
Body
| Field | Type | |
|---|---|---|
command | string | Up to 10,000 characters |
cwd | string | Optional. Default /workspace/repo |
Returns
{ "id": "…", "pid": 123, "command": "npm run dev", "status": "running" }/api/v1/sandboxes/:id/processesThe box's background processes.
Operation sandbox.read
Returns
{ "processes": [{ "id": "…", "pid": 123, "command": "…", "status": "…", "exitCode": null }] }/api/v1/sandboxes/:id/processes/:process/logsA background process's output so far.
Operation sandbox.read
Returns
{ "stdout": "…", "stderr": "…" }/api/v1/sandboxes/:id/processes/:processStop a background process.
Operation sandbox.exec
Returns
{ "ok": true }/api/v1/sandboxes/:id/filesList a folder.
Operation sandbox.read
Query
| Field | Type | |
|---|---|---|
path | string | Optional. Default /workspace/repo |
hidden | 1 | Optional. Include hidden files |
Returns
{ "path": "…", "files": [{ "name": "…", "path": "…", "type": "file", "size": 120, "modifiedAt": "…" }] }/api/v1/sandboxes/:id/files/contentRead a file.
Operation sandbox.read
Query
| Field | Type | |
|---|---|---|
path | string | Required |
encoding | base64 | Optional. Default utf-8 |
Returns
{ "path": "…", "content": "…", "encoding": "utf-8" }/api/v1/sandboxes/:id/files/contentCreate or replace a file. The key also needs sandbox.read.
Operation sandbox.files.write
Body
| Field | Type | |
|---|---|---|
path | string | Up to 1,000 characters |
content | string | Up to 5,000,000 characters |
encoding | string | Optional. utf-8 or base64 |
Returns
{ "ok": true, "path": "…" }/api/v1/sandboxes/:id/files/contentDelete a file.
Operation sandbox.files.write
Query
| Field | Type | |
|---|---|---|
path | string | Required |
Returns
{ "ok": true, "path": "…" }/api/v1/sandboxes/:id/previewsA private link to a port in the box. Anyone with the link can open it until the box stops or the link is closed. The key also needs sandbox.read.
Operation sandbox.preview
Body
| Field | Type | |
|---|---|---|
port | integer | 1024 to 65535 |
Returns
{ "port": 5173, "url": "https://…attackdesk.app" }/api/v1/sandboxes/:id/previews/:portClose a preview link.
Operation sandbox.preview
Returns
{ "ok": true }/api/v1/sandboxes/:id/pushCommit everything in /workspace/repo and push the box's branch, through the code server's usual checks, as the key's person. The key also needs sandbox.read.
Operation sandbox.push
Body
| Field | Type | |
|---|---|---|
message | string | The commit message, 1 to 2,000 characters |
Returns
{ "pushed": true, "branch": "…", "exitCode": 0, "output": "…" }- The key's person needs Push and upload code.
/api/v1/sandboxes/:id/stopStop the box now. Billing stops, and its files are gone.
Operation sandbox.stop
Returns
Box/api/v1/sandboxes/:idStop the box if it's running and remove it from the list.
Operation sandbox.delete
Returns
{ "ok": true }MCP
An MCP server for agents, with AI worker boxes as tools. Streamable HTTP, answered as plain JSON (no event stream).
/api/v1/mcpJSON-RPC 2.0 with the methods initialize, ping, tools/list and tools/call. Send a person's key as the Bearer token.
No operation of its own
Body
| Field | Type | |
|---|---|---|
jsonrpc | string | "2.0" |
id | string | number | Leave it out for a notification. If every message is a notification, the answer is 202 with no body |
method | string | initialize, ping, tools/list or tools/call |
params | object | For tools/call: { name, arguments } |
Returns
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [{ "type": "text", "text": "<the route's JSON answer>" }], "isError": false } }- No operation of its own: each tool call is sent as the matching /api/v1/sandboxes request with the same key, so it needs that route's operation and passes the same checks and limits.
- An array of messages is answered with an array. A GET answers 405.
- A missing or refused key answers 401 with a JSON-RPC error (code -32001).
- isError is true when the route answered 400 or more.
Tools
| Tool | Route | Arguments |
|---|---|---|
list_boxes | GET /sandboxes | all |
start_box | POST /sandboxes | size, branch, maxMinutes, budget, idleMinutes, label |
get_box | GET /sandboxes/:id | box |
run_command | POST /sandboxes/:id/exec | box, command, cwd, timeoutMs |
start_process | POST /sandboxes/:id/processes | box, command, cwd |
list_processes | GET /sandboxes/:id/processes | box |
process_logs | GET /sandboxes/:id/processes/:process/logs | box, process |
stop_process | DELETE /sandboxes/:id/processes/:process | box, process |
list_files | GET /sandboxes/:id/files | box, path |
read_file | GET /sandboxes/:id/files/content | box, path |
write_file | PUT /sandboxes/:id/files/content | box, path, content |
delete_file | DELETE /sandboxes/:id/files/content | box, path |
open_preview | POST /sandboxes/:id/previews | box, port |
push | POST /sandboxes/:id/push | box, message |
box_activity | GET /sandboxes/:id/events | box |
stop_box | POST /sandboxes/:id/stop | box |
Publishing
Each company has one preview address (pv-….attackdesk.app) with its own sign-in, test database and storage. Each preview replaces the one before. Publishing works as one person: use a person's own key. Every publishing request counts as an API call and against the 120-a-minute limit.
/api/v1/publish/previewBuild one commit of your company's code and put it on the preview address. The deploy is charged now; a publish that fails gives it back. The key also needs code.publish.read.
Operation code.publish.preview
Body
| Field | Type | |
|---|---|---|
commit | string | The full 40-character commit id. No other fields |
Returns
202 Publish- The key's person needs Push and upload code. The company needs a card on file (402
card_required). - The commit must be in this company's code (404
commit_not_found) and its company file must name this company (403company_file). - 503
unavailablewhen publishing isn't switched on or couldn't start (nothing is charged).
/api/v1/publishThis company's last 50 publishes, newest first.
Operation code.publish.read
Returns
{ "publishes": [ Publish ] }/api/v1/publish/:idOne publish and where it stands.
Operation code.publish.read
Returns
{
"id": "pub_…",
"kind": "preview",
"commit": "<40-character commit id>",
"status": "queued", // queued | building | uploading | live | replaced | failed
"address": "https://pv-….attackdesk.app",
"error": null,
"createdAt": "2026-09-27T17:00:00.000Z",
"finishedAt": null
}Phone & texts
Numbers, texts, calls and texting registration through your account. Money amounts ending in Micros are millionths of a dollar.
/api/v1/phone/stateThe company's numbers, its business details for registration, and what the next number costs.
Operation phone.overview.read
Returns
{
"testMode": false, "mock": false,
"balanceMicros": 0, "availableMicros": 0, "paysByCard": true, "cardFailing": false,
"nextNumberMicros": 0, "nextTollFreeMicros": 0,
"numbers": [{ "id": "…", "number": "+1…", "kind": "local", "locality": "…", "region": "AZ",
"sms": true, "mms": true, "voice": true, "monthlyMicros": 0, "paidThrough": "…", "boughtAt": "…" }],
"pagesPassedAt": null,
"texting": "not_sent", // not_sent | waiting | action_needed | rejected | approved
"business": { … } | null
}/api/v1/phone/messagesTexts in and out: the newest 200 created after since, returned oldest first.
Operation phone.messages.read
Query
| Field | Type | |
|---|---|---|
since | string | Optional. Only rows created after this createdAt value |
Returns
{ "messages": [{ "id": "…", "numberId": "…", "direction": "in", "from": "+1…", "to": "+1…",
"body": "…", "media": 0, "status": "…", "errorCode": null, "apiKeyId": null, "createdAt": "…", … }] }/api/v1/phone/messagesSend a text from one of the company's numbers. It's charged at the texting prices on the dashboard's Rates page.
Operation phone.messages.send
Body
| Field | Type | |
|---|---|---|
to | string | A full phone number |
body | string | Up to 1,600 characters |
from | string | Optional. One of your texting numbers, or its id. Default: the first one |
Returns
{ "id": "…", "status": "queued", "from": "+1…", "to": "+1…", "body": "…" }- Opted out: 409
opted_out. No texting number: 409no_number. A number you can't text: 400bad_number. - When the texting rules' opt-out line is on, the first text to someone gets it added unless the body already says STOP.
/api/v1/phone/callsCalls to the company's numbers: the newest 200 created after since, returned oldest first.
Operation phone.calls.read
Query
| Field | Type | |
|---|---|---|
since | string | Optional. Only rows created after this createdAt value |
Returns
{ "calls": [{ "id": "…", "direction": "in", "from": "+1…", "to": "+1…", "forwardedTo": "…",
"status": "…", "outcome": "answered", // answered | voicemail | missed
"seconds": 42, "recorded": true, "recordingSeconds": 40, "voicemailSeconds": null,
"chargedMicros": 0, "createdAt": "…", … }] }/api/v1/phone/settingsThe texting rules and each number's call settings.
Operation phone.overview.read
Returns
{
"texting": { "optOutReply": "…", "optInReply": "…", "helpReply": "…", "suggested": { … },
"extraOptOutWords": [], "optOutLine": true, "repliesAppliedAt": null },
"numbers": [{ "id": "…", "number": "+1…", "voice": true, "sms": true, …its call settings }]
}/api/v1/phone/settingsChange the texting rules, and call routing and recording for any number. Send only what changes.
Operation phone.settings.write
Body
| Field | Type | |
|---|---|---|
texting.optOutReply, optInReply, helpReply | string | Up to 320 characters each |
texting.extraOptOutWords | string[] | Up to 20 words |
texting.optOutLine | boolean | |
numbers[].id | string | Which number (up to 50 numbers) |
numbers[].label | string | null | Up to 40 characters |
numbers[].ringMode | string | all (at once) or order (one after another) |
numbers[].ringTo | { phone, name? }[] | Up to 5. None: straight to voicemail |
numbers[].ringSeconds | integer | 10 to 60 |
numbers[].screenCalls | boolean | Whoever answers presses 1 |
numbers[].recordCalls | boolean | The first time, also send acknowledgeRecording: true |
numbers[].recordingNotice | string | Up to 300 characters |
numbers[].voicemail, voicemailGreeting | boolean, string | null | Greeting up to 500 characters |
numbers[].optOutReply | string | null | Up to 320 characters |
Returns
The same as GET/api/v1/phone/opt-outsWho opted out of texts (and back in), newest first (up to 500).
Operation phone.overview.read
Returns
{ "optOuts": [{ "phone": "+1…", "status": "out", "keyword": "STOP", "source": "text", "at": "…", "since": "…" }] }/api/v1/phone/opt-outsStop texting a number.
Operation phone.optouts.add
Body
| Field | Type | |
|---|---|---|
phone | string | A full phone number |
Returns
The same as GET/api/v1/phone/opt-outsUndo an opt-out you added. People who texted STOP can only opt back in themselves, by texting START (409).
Operation phone.optouts.remove
Body
| Field | Type | |
|---|---|---|
phone | string | A full phone number |
Returns
The same as GET/api/v1/phone/optinsEveryone who signed up for texts, newest first (up to 500).
Operation phone.overview.read
Returns
{ "optins": [{ "id": "…", "phone": "+1…", "firstName": "…", "service": true, "offers": false, "method": "…", "sourceUrl": "…", "createdAt": "…" }] }/api/v1/phone/searchUS numbers available to buy. 10 searches a minute per company.
Operation phone.numbers.search
Query
| Field | Type | |
|---|---|---|
kind | string | local or toll_free. Default local |
areaCode | string | 3 digits (local) |
contains | string | 2 to 10 digits or * |
region | string | 2 letters, such as AZ (local) |
Returns
{ "numbers": [{ "number": "+1…", "kind": "local", "locality": "…", "region": "AZ", "sms": true, "mms": true, "voice": true }] }/api/v1/phone/buyBuy a number from a search. Its first month is charged now; the monthly rent goes on while you keep it.
Operation phone.numbers.buy
Body
| Field | Type | |
|---|---|---|
number | string | +1 and 10 digits |
locality | string | null | Optional |
region | string | null | Optional |
Returns
201 { "number": { "id": "…", "number": "+1…", "kind": "local", …the number's record } }/api/v1/phone/releaseGive a number up. It's gone for good.
Operation phone.numbers.release
Body
| Field | Type | |
|---|---|---|
id | string | The number's id |
Returns
{ "ok": true }/api/v1/phone/destinationsWhere texts and calls can go: always the US and Canada, the countries you can switch on with their prices, and any support added.
Operation phone.destinations.read
Returns
{ "alwaysOn": [{ "code": "US", "name": "…" }],
"selfServe": [{ "code": "GB", "name": "…", "prefixes": ["…"], "on": false, "price": … }],
"support": [{ "code": "…", "name": "…", "prefixes": ["…"], "textMicros": 0, "callMicros": 0 }],
"pricing": { "markup": …, "twilioPricesFrom": "…" } }/api/v1/phone/destinationsSwitch a country on or off. Only countries in selfServe; anything else answers 403 contact_support.
Operation phone.destinations.write
Body
| Field | Type | |
|---|---|---|
country | string | Such as "GB" |
on | boolean |
Returns
The same as GET/api/v1/phone/recordingsCalls that still have a recording or voicemail, newest first.
Operation phone.recordings.read
Query
| Field | Type | |
|---|---|---|
before | string | Optional. Only calls created before this createdAt value |
limit | integer | 1 to 100. Default 50 |
Returns
{ "recordings": [{ "id": "<callId>", "direction": "in", "from": "+1…", "to": "+1…", "createdAt": "…",
"recording": { "seconds": 40 } | null, "voicemail": { "seconds": 12 } | null }] }/api/v1/phone/recordings/:callIdThe audio, as MP3 (audio/mpeg).
Operation phone.recordings.read
Query
| Field | Type | |
|---|---|---|
kind | string | call or voicemail. Default call |
/api/v1/phone/recordings/:callIdDelete a recording or voicemail for good. The call stays in the list of calls.
Operation phone.recordings.delete
Query
| Field | Type | |
|---|---|---|
kind | string | call, voicemail or both. Default both |
Returns
{ "deleted": 1 }/api/v1/phone/recordings/keepHow long recordings are kept.
Operation phone.recordings.read
Returns
{ "days": 90 } // 30 | 90 | 365 | null (until deleted)/api/v1/phone/recordings/keepChange how long recordings are kept. Older ones are deleted nightly. Kept minutes are charged daily.
Operation phone.recordings.keep
Body
| Field | Type | |
|---|---|---|
days | 30 | 90 | 365 | null | null keeps them until they're deleted |
Returns
{ "days": 90 }/api/v1/phone/businessSave the business details carriers register.
Operation phone.registration.prepare
Body
| Field | Type | |
|---|---|---|
legalName | string | Required |
brandName | string | Optional |
entityType | string | llc, corporation, partnership, cooperative, nonprofit or sole_proprietor |
ein | string | The 9-digit EIN. Not needed for a sole proprietor |
industry | string | An industry code, such as CONSTRUCTION |
website | string | Optional |
street, street2, city, region, postalCode, country | string | street2 optional. country defaults to US |
repFirstName, repLastName, repEmail, repPhone | string | The person carriers contact |
repTitle | string | Optional |
repPosition | string | null | Optional. CEO, CFO, GM, VP, Director, General Counsel or Other |
stockExchange, stockTicker | string | Optional |
Returns
{ "ok": true }- A body that doesn't check out answers 400 with fields: a message for each field.
/api/v1/phone/pagesThe texting pages carriers read: where they live, what they say, and whether they passed the check.
Operation phone.overview.read
Returns
{ …the pages settings, "domain": { "hostname": "…", "status": "…", "error": null, "target": "…" } | null,
"suggestedHostname": "…", "liveUrls": { … }, "previewUrls": { … } }/api/v1/phone/pagesChange the texting pages. Save the business details first.
Operation phone.registration.prepare
Body
| Field | Type | |
|---|---|---|
mode | string | hosted or own_site |
hostname | string | null | Optional |
services | string | One sentence about what you do, 10 to 200 characters |
offers | boolean | |
offersPerMonth | integer | 1 to 30 |
optinPhone, optinPaper | boolean | |
supportPhone | string | A number customers can call |
supportEmail | string | An address customers can write to |
ownTextUrl, ownTermsUrl, ownPrivacyUrl | string | null | Optional URLs |
Returns
The same as GET/api/v1/phone/pages/checkRead the live pages the way carriers will, and list anything they'd turn down.
Operation phone.registration.prepare
Returns
{ "passed": false, "problems": [ … ], "domainStatus": "connected", "domainError": null }- Before the pages' domain is connected, passed is false and domainStatus says where it stands.
/api/v1/phone/campaignHow you'll text: the draft or saved campaign, what the check found, the fees and the automatic replies.
Operation phone.overview.read
Returns
{ "ready": true, "saved": false, "brandType": "…", "setupPaid": false, "directLending": false,
"description": "…", "messageFlow": "…", "samples": ["…"], "hasLinks": false, "hasPhone": false,
"problems": [ … ], "fees": { … }, "mock": false, "replies": { … }, … }
// something still missing first: { "ready": false, "missing": … }/api/v1/phone/campaignSave the campaign. It can't change while it's with the carriers.
Operation phone.registration.prepare
Body
| Field | Type | |
|---|---|---|
brandType | string | low_volume, standard or sole_proprietor |
description | string | 40 to 4,096 characters |
messageFlow | string | 40 to 2,048 characters |
samples | string[] | 2 to 5 sample texts, 20 to 1,024 characters each |
directLending | boolean |
Returns
The same as GET/api/v1/phone/campaign/draftA fresh draft from the business details and pages, not saved.
Operation phone.registration.prepare
Returns
{ "description": "…", "messageFlow": "…", "samples": ["…"], "hasLinks": false, "hasPhone": false,
"directLending": false, "optInMessage": "…", "privacyUrl": "…", … }/api/v1/phone/registrationWhere carrier registration stands, piece by piece, with a plain-English fix for anything turned down.
Operation phone.overview.read
Returns
{ "testMode": false, "mock": false, "overall": "…", "parts": [ … ], "calling": … , "paysByCard": … }/api/v1/phone/registration/businessSend the business for review. The brand fee is charged. Sends again after a fix.
Operation phone.registration.submit
Body
| Field | Type | |
|---|---|---|
brandType | string | low_volume, standard or sole_proprietor |
live | boolean | Optional. Replace a free test brand with a paid live registration |
Returns
The same as GET /api/v1/phone/registration- Owners and admins only (403
role): the texting setup is charged to the card on file.
/api/v1/phone/registrationSend the campaign (or send it again after a fix), after the business. Its fee, and $29 for rush, are charged.
Operation phone.registration.submit
Body
| Field | Type | |
|---|---|---|
rush | boolean | Optional |
Returns
The same as GET/api/v1/phone/registration/confirmTest mode only: stands in for the carriers once the owner has confirmed.
Operation phone.registration.submit
Returns
The same as GET /api/v1/phone/registrationSending from your connected domain.
/api/v1/email/sendSend an email. Each recipient is charged.
Operation email.send
Body
| Field | Type | |
|---|---|---|
from | string | An address at a verified sending domain. "Name <address>" works |
to | string | string[] | One address, or 1 to 50 |
subject | string | 1 to 998 characters |
text | string | Up to 200,000 characters |
html | string | Up to 500,000 characters. Send text, html or both |
replyTo | string | Optional |
Returns
{ "delivered": [ … ], "queued": [ … ], "bounced": [ … ] }- A from address that isn't at a verified sending domain, or a refused send, answers 400
not_sent.
/api/v1/email/domainsYour connected domains and their sending domains.
Operation email.domains.read
Returns
{ "domains": [{ "id": "…", "domain": "example.com", "status": "…", "suggestedSender": "m.example.com" }],
"senders": [{ "id": "…", "domainId": "…", "domain": "m.example.com", "status": "verified", "error": null,
"checkedAt": "…", "parentDomain": "example.com", "parentStatus": "…" }] }/api/v1/email/domainsAdd a sending domain: { "action": "add" }.
Operation email.domains.add
Body
| Field | Type | |
|---|---|---|
action | "add" | |
domainId | string | A connected domain's id |
domain | string | A name under it, such as m.example.com |
Returns
{ "id": "…", "domain": "m.example.com", "status": "…" }/api/v1/email/domainsCheck a sending domain: { "action": "check" }.
Operation email.domains.check
Body
| Field | Type | |
|---|---|---|
action | "check" | |
id | string | The sending domain's id |
Returns
{ "status": "verified" }Domains & DNS
Your company's connected domains. The domain stays registered where it is. Every POST /api/v1/domain body is one of the shapes below; the one you send decides the operation.
/api/v1/domainA domain's status, our nameservers, where to change them, its records and app addresses.
Operation dns.read
Query
| Field | Type | |
|---|---|---|
domainId | string | Optional. Default: the first domain |
Returns
{
"id": "…", "domain": "example.com", "status": "…",
"nameServers": ["…"], "replaces": ["…"], "registrar": …, "dnssecOn": false,
"email": "…",
"domains": [{ "id": "…", "domain": "example.com", "status": "…" }],
"records": [ … ],
"appAddresses": [{ "id": "…", "hostname": "…", "status": "…", "error": null }]
}
// no domain connected yet: { "domain": null, "domains": [] }/api/v1/domainConnect a domain. Its records are copied first. 6 a minute per company (with checks).
Operation dns.domains.attach
Body
| Field | Type | |
|---|---|---|
domain | string | Such as example.com |
Returns
The same as GET, for the new domain/api/v1/domainHas the nameserver change gone through? When the domain is live, this also sets up its email, if the key may add a sending domain.
Operation dns.check
Body
| Field | Type | |
|---|---|---|
check | true | |
domainId | string | Optional |
Returns
{ "status": "…", "seen": …, "pointed": … }/api/v1/domainAdd a record: { "action": "add_record" }.
Operation dns.records.write
Body
| Field | Type | |
|---|---|---|
action | "add_record" | |
record.type | string | A, AAAA, CNAME, MX or TXT |
record.name | string | Up to 253 characters |
record.content | string | 1 to 2,048 characters |
record.priority | number | null | Optional, 0 to 65,535 |
domainId | string | Optional. Default: the company's first domain |
Returns
{ "ok": true, "records": [ … ] }/api/v1/domainChange a record: { "action": "update_record" }.
Operation dns.records.write
Body
| Field | Type | |
|---|---|---|
action | "update_record" | |
id | string | The record's id |
record.type | string | A, AAAA, CNAME, MX or TXT |
record.name | string | Up to 253 characters |
record.content | string | 1 to 2,048 characters |
record.priority | number | null | Optional, 0 to 65,535 |
domainId | string | Optional. Default: the company's first domain |
Returns
{ "ok": true, "records": [ … ] }/api/v1/domainRemove a record: { "action": "remove_record" }.
Operation dns.records.write
Body
| Field | Type | |
|---|---|---|
action | "remove_record" | |
id | string | The record's id |
domainId | string | Optional |
Returns
{ "ok": true, "records": [ … ] }/api/v1/domainExport the records as a standard zone file: { "action": "export_records" }.
Operation dns.read
Body
| Field | Type | |
|---|---|---|
action | "export_records" | |
domainId | string | Optional |
Returns
{ "domain": "example.com", "file": "<zone file>" }/api/v1/domainPoint a web address at your app: { "action": "save_app_address" }. The key also needs dns.records.write.
Operation hosting.appAddress.write
Body
| Field | Type | |
|---|---|---|
action | "save_app_address" | |
domainId | string | A connected domain's id |
hostname | string | The address, such as app.example.com |
Returns
{ "id": "…", "hostname": "app.example.com", "status": "waiting_dns", … }Address lookup
Address suggestions and details through Google Places. Use the same sessionToken for the suggestions and the pick. Both are charged per call and count against the 120-a-minute limit.
/api/v1/placesSuggestions as someone types: { "action": "suggest" }.
Operation lookup.address.suggest
Body
| Field | Type | |
|---|---|---|
action | "suggest" | |
input | string | 1 to 200 characters |
sessionToken | string | 8 to 100 characters |
Returns
{ "result": [{ "placeId": "…", "main": "…", "secondary": "…" }] }/api/v1/placesThe address picked: { "action": "details" }. Asking again for the same pick in the same session answers from the kept result, with no new charge.
Operation lookup.address.details
Body
| Field | Type | |
|---|---|---|
action | "details" | |
placeId | string | From a suggestion |
sessionToken | string | 8 to 100 characters |
Returns
{ "result": { "line1": "…", "city": "…", "state": "AZ", "zip": "…", "country": "US",
"lat": 33.4, "lng": -112.0, "formatted": "…" } | null }- The same pick still loading from another call: 429
lookup_limit.
Website leads
Leads AttackDesk is holding for your company, for your app to collect. The old website form routes (/api/v1/leads/intake, /api/v1/leads/forms) are retired and answer 410.
/api/v1/leadsLeads created after since, oldest first (up to 200).
Operation crm.leads.read
Query
| Field | Type | |
|---|---|---|
since | string | Optional. Only rows created after this createdAt value |
Returns
{ "leads": [{ "id": "…", "formId": "…", "firstName": "…", "lastName": "…", "email": "…", "phone": "+1…",
"company": "…", "message": "…", "fields": { … }, "pageUrl": "…", "formName": "…",
"smsConsent": "…", "emailConsent": "…", "utm": { … }, "collectedAt": null, "createdAt": "…", … }] }/api/v1/leadsMark leads collected. AttackDesk keeps them for the records.
Operation crm.leads.collect
Body
| Field | Type | |
|---|---|---|
collected | string[] | Lead ids, up to 500 |
Returns
{ "collected": 3 }Account & billing
The balance, buying funds with a key, affiliate earnings, and deleting the company.
/api/v1/creditsThe balance in dollars, and what this key may still buy this month.
Operation account.balance.read
Returns
{ "balance": 12.5,
"purchases": "off", // on | off | needs_owner_review
"perPurchaseLimit": null, "monthlyLimit": null,
"boughtThisMonth": 0, "pending": 0, "remainingThisMonth": 0,
"cardOnFile": null }/api/v1/creditsBuy funds with this key: the card on file, or the buyer's own wallet through a Stripe shared payment token. Not an operation: an owner turns on Add funds for the key, with a limit for one purchase and for the month. Send an Idempotency-Key header: a new one for each purchase, the same one only to retry it.
No operation of its own
Body
| Field | Type | |
|---|---|---|
amount | number | Dollars |
sharedPaymentToken | string | Optional. spt_… |
Returns
200 (paid) or 202 (not yet)
{ "status": "paid", "amount": 25, "purchaseId": "…", "paymentId": "…", "balance": 37.5 }
// status: paid | processing | unconfirmed | in_progress- Refusals: 403
purchase_not_allowed,purchase_needs_review,over_purchase_limit,over_monthly_limit; 400bad_amount,idempotency_key_required; 409idempotency_conflict; 402no_card,needs_owner,declined,payment_failed.
/api/v1/affiliateThe company's affiliate link, clicks, referred companies and earnings. Joining happens on the dashboard.
Operation account.affiliate.read
Returns
{ "joined": true, "link": "…", "status": "…", "clicks": 0,
"referrals": [{ "company": "…", "signedUp": "…", "state": "…", "underReview": false, "earned": 0 }],
"earnings": { "onHold": 0, "readyToSend": 0, "sentToStripe": 0, … },
"payouts": { "setUp": false, "connectType": "…", "enabled": false, "next": "…", "minimum": 50 } }
// not joined: { "joined": false, … }/api/v1/company/deleteDelete the key's company. Its keys stop at once; its data is kept 14 days, then deleted. The key's person must be an owner.
Operation account.company.delete
Body
| Field | Type | |
|---|---|---|
confirm | string | The company's name |
Returns
{ "deleteDataAfter": "…", "keysSwitchedOff": 3 }- Not an owner, or the wrong name: 403
not_allowed.
App connection
What a connected copy of AttackDesk calls to stay connected. These don't count as API calls.
/api/v1/accountWhat the app's Account page shows.
Operation connection.account.read
Returns
{ "company": "…", "plan": "managed", "billing": "…", "balanceMicros": 0, "person": "…", "latest": "…", "channel": "…" }/api/v1/checkinThe daily check-in: version and counts, never content.
Operation connection.checkin
Body
| Field | Type | |
|---|---|---|
installId | string | null | 56 hex characters; null the first time |
version | string | |
platform | string | null | Optional |
seal | string | Optional. The protected core's fingerprint |
counts | object | Optional: projects, people, contacts, companies, deals, messages, appointments, automations |
Returns
{ "installId": "…", "companyId": "…", "latest": "…", "channel": "…", "seal": "…" }/api/v1/updatesUpdates newer than a version, oldest first.
Operation connection.updates.read
Query
| Field | Type | |
|---|---|---|
version | string | Required, such as 0.1.5 |
Returns
{ "updates": [{ "version": "…", "from": "…", "notes": "…", "publishedAt": "…", "required": false }],
"codeBranch": { "version": "…", "status": "clean", "required": false } | null }/api/v1/device-updatesA device's update status. The body is at most 4 KB and takes no other fields.
Operation connection.devices.report
Body
| Field | Type | |
|---|---|---|
installId | string | 56 hex characters |
eventId, attemptId | string | UUIDs |
sequence | integer | Positive |
kind | string | app or launcher |
version | string | |
status | string | available, deferred, started, completed or failed |
reason | string | Optional: user_deferred, download_failed, verification_failed, install_failed, incompatible, unknown |
Returns
{ "ok": true, "replayed": false }Coming
These operations are in the catalogue but have no route yet. Keys can't be given them.
| Operation | What |
|---|---|
ai.models.use | Use AI models through a key |
phone.calls.place | Place calls |
email.status.read | Read email delivery status |
email.templates.manage | Manage email templates |
email.domains.remove | Remove a sending domain |
code.publish | Publish a live company version |
hosting.apps.read, hosting.deploy, hosting.rollback, hosting.restart, hosting.delete | See, deploy, roll back, restart and delete hosted apps |
automations.read, automations.create, automations.run, automations.schedule | Automations and jobs |
Some things stay on the signed-in dashboard only: managing keys and the team, usage and spending limits, payment methods and the plan, downloading and uploading the code, creating the repository, the release channel, removing a domain and sending a test email.