AttackDeskDocs

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. Send Authorization: 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. A Box and a Publish are shown under their routes.

People

Who is on the key's company, and who's online.

GET/api/v1/people

Everyone 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>.git

Clone, 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 push

Push 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.

GET/api/v1/sandboxes

Your boxes, newest first (up to 50). Stopped ones stay in the list until deleted.

Operation sandbox.read

Query

FieldType
all1Optional. Everyone's boxes, for owners and admins

Returns

{ "boxes": [ Box ] }
POST/api/v1/sandboxes

Start 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

FieldType
sizestringsmall, medium, large or xlarge. Default medium
branchstringDefault main
maxMinutesinteger1 to 720. Default 60
budgetnumber | nullDollars, 0.01 to 500. Default none
idleMinutesinteger5 to 60. Default 15
labelstring | nullUp 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: 502 clone_failed. A box that can't start isn't charged (503 box_unavailable).
GET/api/v1/sandboxes/:id

One 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 }
}
GET/api/v1/sandboxes/:id/events

Everything 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": "…" }] }
POST/api/v1/sandboxes/:id/exec

Run a shell command and wait for it.

Operation sandbox.exec

Body

FieldType
commandstringUp to 10,000 characters
cwdstringOptional. Default /workspace/repo
timeoutMsintegerOptional. 1,000 to 600,000. Default 120,000

Returns

{ "exitCode": 0, "success": true, "stdout": "…", "stderr": "" }
  • stdout and stderr are cut at 200,000 characters.
POST/api/v1/sandboxes/:id/processes

Start a command in the background, such as a dev server.

Operation sandbox.exec

Body

FieldType
commandstringUp to 10,000 characters
cwdstringOptional. Default /workspace/repo

Returns

{ "id": "…", "pid": 123, "command": "npm run dev", "status": "running" }
GET/api/v1/sandboxes/:id/processes

The box's background processes.

Operation sandbox.read

Returns

{ "processes": [{ "id": "…", "pid": 123, "command": "…", "status": "…", "exitCode": null }] }
GET/api/v1/sandboxes/:id/processes/:process/logs

A background process's output so far.

Operation sandbox.read

Returns

{ "stdout": "…", "stderr": "…" }
DELETE/api/v1/sandboxes/:id/processes/:process

Stop a background process.

Operation sandbox.exec

Returns

{ "ok": true }
GET/api/v1/sandboxes/:id/files

List a folder.

Operation sandbox.read

Query

FieldType
pathstringOptional. Default /workspace/repo
hidden1Optional. Include hidden files

Returns

{ "path": "…", "files": [{ "name": "…", "path": "…", "type": "file", "size": 120, "modifiedAt": "…" }] }
GET/api/v1/sandboxes/:id/files/content

Read a file.

Operation sandbox.read

Query

FieldType
pathstringRequired
encodingbase64Optional. Default utf-8

Returns

{ "path": "…", "content": "…", "encoding": "utf-8" }
PUT/api/v1/sandboxes/:id/files/content

Create or replace a file. The key also needs sandbox.read.

Operation sandbox.files.write

Body

FieldType
pathstringUp to 1,000 characters
contentstringUp to 5,000,000 characters
encodingstringOptional. utf-8 or base64

Returns

{ "ok": true, "path": "…" }
DELETE/api/v1/sandboxes/:id/files/content

Delete a file.

Operation sandbox.files.write

Query

FieldType
pathstringRequired

Returns

{ "ok": true, "path": "…" }
POST/api/v1/sandboxes/:id/previews

A 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

FieldType
portinteger1024 to 65535

Returns

{ "port": 5173, "url": "https://…attackdesk.app" }
DELETE/api/v1/sandboxes/:id/previews/:port

Close a preview link.

Operation sandbox.preview

Returns

{ "ok": true }
POST/api/v1/sandboxes/:id/push

Commit 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

FieldType
messagestringThe commit message, 1 to 2,000 characters

Returns

{ "pushed": true, "branch": "…", "exitCode": 0, "output": "…" }
  • The key's person needs Push and upload code.
POST/api/v1/sandboxes/:id/stop

Stop the box now. Billing stops, and its files are gone.

Operation sandbox.stop

Returns

Box
DELETE/api/v1/sandboxes/:id

Stop 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).

POST/api/v1/mcp

JSON-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

FieldType
jsonrpcstring"2.0"
idstring | numberLeave it out for a notification. If every message is a notification, the answer is 202 with no body
methodstringinitialize, ping, tools/list or tools/call
paramsobjectFor 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

ToolRouteArguments
list_boxesGET /sandboxesall
start_boxPOST /sandboxessize, branch, maxMinutes, budget, idleMinutes, label
get_boxGET /sandboxes/:idbox
run_commandPOST /sandboxes/:id/execbox, command, cwd, timeoutMs
start_processPOST /sandboxes/:id/processesbox, command, cwd
list_processesGET /sandboxes/:id/processesbox
process_logsGET /sandboxes/:id/processes/:process/logsbox, process
stop_processDELETE /sandboxes/:id/processes/:processbox, process
list_filesGET /sandboxes/:id/filesbox, path
read_fileGET /sandboxes/:id/files/contentbox, path
write_filePUT /sandboxes/:id/files/contentbox, path, content
delete_fileDELETE /sandboxes/:id/files/contentbox, path
open_previewPOST /sandboxes/:id/previewsbox, port
pushPOST /sandboxes/:id/pushbox, message
box_activityGET /sandboxes/:id/eventsbox
stop_boxPOST /sandboxes/:id/stopbox

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.

POST/api/v1/publish/preview

Build 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

FieldType
commitstringThe 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 (403 company_file).
  • 503 unavailable when publishing isn't switched on or couldn't start (nothing is charged).
GET/api/v1/publish

This company's last 50 publishes, newest first.

Operation code.publish.read

Returns

{ "publishes": [ Publish ] }
GET/api/v1/publish/:id

One 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.

GET/api/v1/phone/state

The 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
}
GET/api/v1/phone/messages

Texts in and out: the newest 200 created after since, returned oldest first.

Operation phone.messages.read

Query

FieldType
sincestringOptional. 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": "…", … }] }
POST/api/v1/phone/messages

Send 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

FieldType
tostringA full phone number
bodystringUp to 1,600 characters
fromstringOptional. 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: 409 no_number. A number you can't text: 400 bad_number.
  • When the texting rules' opt-out line is on, the first text to someone gets it added unless the body already says STOP.
GET/api/v1/phone/calls

Calls to the company's numbers: the newest 200 created after since, returned oldest first.

Operation phone.calls.read

Query

FieldType
sincestringOptional. 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": "…", … }] }
GET/api/v1/phone/settings

The 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 }]
}
PUT/api/v1/phone/settings

Change the texting rules, and call routing and recording for any number. Send only what changes.

Operation phone.settings.write

Body

FieldType
texting.optOutReply, optInReply, helpReplystringUp to 320 characters each
texting.extraOptOutWordsstring[]Up to 20 words
texting.optOutLineboolean
numbers[].idstringWhich number (up to 50 numbers)
numbers[].labelstring | nullUp to 40 characters
numbers[].ringModestringall (at once) or order (one after another)
numbers[].ringTo{ phone, name? }[]Up to 5. None: straight to voicemail
numbers[].ringSecondsinteger10 to 60
numbers[].screenCallsbooleanWhoever answers presses 1
numbers[].recordCallsbooleanThe first time, also send acknowledgeRecording: true
numbers[].recordingNoticestringUp to 300 characters
numbers[].voicemail, voicemailGreetingboolean, string | nullGreeting up to 500 characters
numbers[].optOutReplystring | nullUp to 320 characters

Returns

The same as GET
GET/api/v1/phone/opt-outs

Who 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": "…" }] }
POST/api/v1/phone/opt-outs

Stop texting a number.

Operation phone.optouts.add

Body

FieldType
phonestringA full phone number

Returns

The same as GET
DELETE/api/v1/phone/opt-outs

Undo an opt-out you added. People who texted STOP can only opt back in themselves, by texting START (409).

Operation phone.optouts.remove

Body

FieldType
phonestringA full phone number

Returns

The same as GET
GET/api/v1/phone/optins

Everyone 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": "…" }] }
GET/api/v1/phone/search

US numbers available to buy. 10 searches a minute per company.

Operation phone.numbers.search

Query

FieldType
kindstringlocal or toll_free. Default local
areaCodestring3 digits (local)
containsstring2 to 10 digits or *
regionstring2 letters, such as AZ (local)

Returns

{ "numbers": [{ "number": "+1…", "kind": "local", "locality": "…", "region": "AZ", "sms": true, "mms": true, "voice": true }] }
POST/api/v1/phone/buy

Buy 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

FieldType
numberstring+1 and 10 digits
localitystring | nullOptional
regionstring | nullOptional

Returns

201  { "number": { "id": "…", "number": "+1…", "kind": "local", …the number's record } }
POST/api/v1/phone/release

Give a number up. It's gone for good.

Operation phone.numbers.release

Body

FieldType
idstringThe number's id

Returns

{ "ok": true }
GET/api/v1/phone/destinations

Where 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": "…" } }
POST/api/v1/phone/destinations

Switch a country on or off. Only countries in selfServe; anything else answers 403 contact_support.

Operation phone.destinations.write

Body

FieldType
countrystringSuch as "GB"
onboolean

Returns

The same as GET
GET/api/v1/phone/recordings

Calls that still have a recording or voicemail, newest first.

Operation phone.recordings.read

Query

FieldType
beforestringOptional. Only calls created before this createdAt value
limitinteger1 to 100. Default 50

Returns

{ "recordings": [{ "id": "<callId>", "direction": "in", "from": "+1…", "to": "+1…", "createdAt": "…",
                  "recording": { "seconds": 40 } | null, "voicemail": { "seconds": 12 } | null }] }
GET/api/v1/phone/recordings/:callId

The audio, as MP3 (audio/mpeg).

Operation phone.recordings.read

Query

FieldType
kindstringcall or voicemail. Default call
DELETE/api/v1/phone/recordings/:callId

Delete a recording or voicemail for good. The call stays in the list of calls.

Operation phone.recordings.delete

Query

FieldType
kindstringcall, voicemail or both. Default both

Returns

{ "deleted": 1 }
GET/api/v1/phone/recordings/keep

How long recordings are kept.

Operation phone.recordings.read

Returns

{ "days": 90 }            // 30 | 90 | 365 | null (until deleted)
POST/api/v1/phone/recordings/keep

Change how long recordings are kept. Older ones are deleted nightly. Kept minutes are charged daily.

Operation phone.recordings.keep

Body

FieldType
days30 | 90 | 365 | nullnull keeps them until they're deleted

Returns

{ "days": 90 }
PUT/api/v1/phone/business

Save the business details carriers register.

Operation phone.registration.prepare

Body

FieldType
legalNamestringRequired
brandNamestringOptional
entityTypestringllc, corporation, partnership, cooperative, nonprofit or sole_proprietor
einstringThe 9-digit EIN. Not needed for a sole proprietor
industrystringAn industry code, such as CONSTRUCTION
websitestringOptional
street, street2, city, region, postalCode, countrystringstreet2 optional. country defaults to US
repFirstName, repLastName, repEmail, repPhonestringThe person carriers contact
repTitlestringOptional
repPositionstring | nullOptional. CEO, CFO, GM, VP, Director, General Counsel or Other
stockExchange, stockTickerstringOptional

Returns

{ "ok": true }
  • A body that doesn't check out answers 400 with fields: a message for each field.
GET/api/v1/phone/pages

The 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": { … } }
PUT/api/v1/phone/pages

Change the texting pages. Save the business details first.

Operation phone.registration.prepare

Body

FieldType
modestringhosted or own_site
hostnamestring | nullOptional
servicesstringOne sentence about what you do, 10 to 200 characters
offersboolean
offersPerMonthinteger1 to 30
optinPhone, optinPaperboolean
supportPhonestringA number customers can call
supportEmailstringAn address customers can write to
ownTextUrl, ownTermsUrl, ownPrivacyUrlstring | nullOptional URLs

Returns

The same as GET
POST/api/v1/phone/pages/check

Read 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.
GET/api/v1/phone/campaign

How 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": … }
PUT/api/v1/phone/campaign

Save the campaign. It can't change while it's with the carriers.

Operation phone.registration.prepare

Body

FieldType
brandTypestringlow_volume, standard or sole_proprietor
descriptionstring40 to 4,096 characters
messageFlowstring40 to 2,048 characters
samplesstring[]2 to 5 sample texts, 20 to 1,024 characters each
directLendingboolean

Returns

The same as GET
POST/api/v1/phone/campaign/draft

A 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": "…", … }
GET/api/v1/phone/registration

Where 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": … }
POST/api/v1/phone/registration/business

Send the business for review. The brand fee is charged. Sends again after a fix.

Operation phone.registration.submit

Body

FieldType
brandTypestringlow_volume, standard or sole_proprietor
livebooleanOptional. 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.
POST/api/v1/phone/registration

Send 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

FieldType
rushbooleanOptional

Returns

The same as GET
POST/api/v1/phone/registration/confirm

Test mode only: stands in for the carriers once the owner has confirmed.

Operation phone.registration.submit

Returns

The same as GET /api/v1/phone/registration

Email

Sending from your connected domain.

POST/api/v1/email/send

Send an email. Each recipient is charged.

Operation email.send

Body

FieldType
fromstringAn address at a verified sending domain. "Name <address>" works
tostring | string[]One address, or 1 to 50
subjectstring1 to 998 characters
textstringUp to 200,000 characters
htmlstringUp to 500,000 characters. Send text, html or both
replyTostringOptional

Returns

{ "delivered": [ … ], "queued": [ … ], "bounced": [ … ] }
  • A from address that isn't at a verified sending domain, or a refused send, answers 400 not_sent.
GET/api/v1/email/domains

Your 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": "…" }] }
POST/api/v1/email/domains

Add a sending domain: { "action": "add" }.

Operation email.domains.add

Body

FieldType
action"add"
domainIdstringA connected domain's id
domainstringA name under it, such as m.example.com

Returns

{ "id": "…", "domain": "m.example.com", "status": "…" }
POST/api/v1/email/domains

Check a sending domain: { "action": "check" }.

Operation email.domains.check

Body

FieldType
action"check"
idstringThe 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.

GET/api/v1/domain

A domain's status, our nameservers, where to change them, its records and app addresses.

Operation dns.read

Query

FieldType
domainIdstringOptional. 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": [] }
POST/api/v1/domain

Connect a domain. Its records are copied first. 6 a minute per company (with checks).

Operation dns.domains.attach

Body

FieldType
domainstringSuch as example.com

Returns

The same as GET, for the new domain
POST/api/v1/domain

Has 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

FieldType
checktrue
domainIdstringOptional

Returns

{ "status": "…", "seen": …, "pointed": … }
POST/api/v1/domain

Add a record: { "action": "add_record" }.

Operation dns.records.write

Body

FieldType
action"add_record"
record.typestringA, AAAA, CNAME, MX or TXT
record.namestringUp to 253 characters
record.contentstring1 to 2,048 characters
record.prioritynumber | nullOptional, 0 to 65,535
domainIdstringOptional. Default: the company's first domain

Returns

{ "ok": true, "records": [ … ] }
POST/api/v1/domain

Change a record: { "action": "update_record" }.

Operation dns.records.write

Body

FieldType
action"update_record"
idstringThe record's id
record.typestringA, AAAA, CNAME, MX or TXT
record.namestringUp to 253 characters
record.contentstring1 to 2,048 characters
record.prioritynumber | nullOptional, 0 to 65,535
domainIdstringOptional. Default: the company's first domain

Returns

{ "ok": true, "records": [ … ] }
POST/api/v1/domain

Remove a record: { "action": "remove_record" }.

Operation dns.records.write

Body

FieldType
action"remove_record"
idstringThe record's id
domainIdstringOptional

Returns

{ "ok": true, "records": [ … ] }
POST/api/v1/domain

Export the records as a standard zone file: { "action": "export_records" }.

Operation dns.read

Body

FieldType
action"export_records"
domainIdstringOptional

Returns

{ "domain": "example.com", "file": "<zone file>" }
POST/api/v1/domain

Point a web address at your app: { "action": "save_app_address" }. The key also needs dns.records.write.

Operation hosting.appAddress.write

Body

FieldType
action"save_app_address"
domainIdstringA connected domain's id
hostnamestringThe 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.

POST/api/v1/places

Suggestions as someone types: { "action": "suggest" }.

Operation lookup.address.suggest

Body

FieldType
action"suggest"
inputstring1 to 200 characters
sessionTokenstring8 to 100 characters

Returns

{ "result": [{ "placeId": "…", "main": "…", "secondary": "…" }] }
POST/api/v1/places

The 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

FieldType
action"details"
placeIdstringFrom a suggestion
sessionTokenstring8 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.

GET/api/v1/leads

Leads created after since, oldest first (up to 200).

Operation crm.leads.read

Query

FieldType
sincestringOptional. 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": "…", … }] }
POST/api/v1/leads

Mark leads collected. AttackDesk keeps them for the records.

Operation crm.leads.collect

Body

FieldType
collectedstring[]Lead ids, up to 500

Returns

{ "collected": 3 }

Account & billing

The balance, buying funds with a key, affiliate earnings, and deleting the company.

GET/api/v1/credits

The 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 }
POST/api/v1/credits

Buy 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

FieldType
amountnumberDollars
sharedPaymentTokenstringOptional. 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; 400 bad_amount, idempotency_key_required; 409 idempotency_conflict; 402 no_card, needs_owner, declined, payment_failed.
GET/api/v1/affiliate

The 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, … }
POST/api/v1/company/delete

Delete 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

FieldType
confirmstringThe 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.

GET/api/v1/account

What the app's Account page shows.

Operation connection.account.read

Returns

{ "company": "…", "plan": "managed", "billing": "…", "balanceMicros": 0, "person": "…", "latest": "…", "channel": "…" }
POST/api/v1/checkin

The daily check-in: version and counts, never content.

Operation connection.checkin

Body

FieldType
installIdstring | null56 hex characters; null the first time
versionstring
platformstring | nullOptional
sealstringOptional. The protected core's fingerprint
countsobjectOptional: projects, people, contacts, companies, deals, messages, appointments, automations

Returns

{ "installId": "…", "companyId": "…", "latest": "…", "channel": "…", "seal": "…" }
GET/api/v1/updates

Updates newer than a version, oldest first.

Operation connection.updates.read

Query

FieldType
versionstringRequired, such as 0.1.5

Returns

{ "updates": [{ "version": "…", "from": "…", "notes": "…", "publishedAt": "…", "required": false }],
  "codeBranch": { "version": "…", "status": "clean", "required": false } | null }
POST/api/v1/device-updates

A device's update status. The body is at most 4 KB and takes no other fields.

Operation connection.devices.report

Body

FieldType
installIdstring56 hex characters
eventId, attemptIdstringUUIDs
sequenceintegerPositive
kindstringapp or launcher
versionstring
statusstringavailable, deferred, started, completed or failed
reasonstringOptional: 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.

OperationWhat
ai.models.useUse AI models through a key
phone.calls.placePlace calls
email.status.readRead email delivery status
email.templates.manageManage email templates
email.domains.removeRemove a sending domain
code.publishPublish a live company version
hosting.apps.read, hosting.deploy, hosting.rollback, hosting.restart, hosting.deleteSee, deploy, roll back, restart and delete hosted apps
automations.read, automations.create, automations.run, automations.scheduleAutomations 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.