Actions
Discover and call the verbs of the Dashly API, the operations a plain field update cannot express.
Much of what the dashboard does is not "change a field": logging a new opportunity, merging two contacts, importing a CSV, reading a client's site statistics. The API exposes these as actions, verbs declared on a resource.
Two kinds of actions
| Target | Path | Example |
|---|---|---|
| The collection | /v1/{resource}/actions/{verb} | POST /v1/deals/actions/new-opportunity |
| One record | /v1/{resource}/{id}/actions/{verb} | POST /v1/contacts/{id}/actions/merge |
Actions that change data are called with POST and a JSON body, need the resource's write scope (for example deals:write), and answer 405 method_not_allowed to a GET.
Read-only actions compute something without changing anything. They need the resource's read scope and answer both GET, with their parameters in the query string, and POST, with a JSON body. A few read-only actions are made of sections that each check their own scope: a section the key may not read is left out of the answer.
A successful action answers 200 with its result in data. Its errors use the usual envelope.
Discover actions
GET /v1 lists the name of every action of every resource. GET /v1/{resource}/actions describes each one, including the JSON Schema of the body it expects:
curl https://api.usedashly.com/v1/deals/actions \
-H "Authorization: Bearer $DASHLY_API_KEY"{
"data": [
{
"name": "new-opportunity",
"label": "Log a new opportunity",
"description": "One sentence about an organisation becomes the right record: …",
"target": "collection",
"readOnly": false,
"path": "/v1/deals/actions/new-opportunity",
"input": {
"type": "object",
"properties": {
"clientId": { "type": "string", "minLength": 1, "maxLength": 60 },
"organisationName": { "type": "string", "minLength": 2, "maxLength": 120 },
"summary": { "type": "string", "minLength": 1, "maxLength": 200 },
"amountCents": { "anyOf": [{ "type": "integer", "minimum": 0, "maximum": 2000000000 }, { "type": "null" }] },
"followUpOn": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
"ownerId": { "type": "string", "minLength": 1, "maxLength": 120 },
"onExistingDeal": { "default": "fail", "type": "string", "enum": ["fail", "create"] }
},
"required": ["summary"]
}
}
// … the other actions of deals
]
}input is null for an action that takes no parameters. The API reference documents every action as an operation too.
An unknown action answers 404 not_found.
Examples
Log a new opportunity
POST /v1/deals/actions/new-opportunity does what the dashboard's "New opportunity" button does: Dashly decides which record to create. An unknown organisation is created with a lead to contact, an existing client gets a deal in its pipeline for existing clients, anyone else a deal in the default pipeline. A follow-up task is always planned for the owner (in 3 days by default).
curl -X POST https://api.usedashly.com/v1/deals/actions/new-opportunity \
-H "Authorization: Bearer $DASHLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: opp-2026-10-29-lumen" \
-d '{
"clientId": "hsbftmp3f5",
"summary": "Website redesign for the spring collection",
"amountCents": 1200000,
"followUpOn": "2026-11-03"
}'{
"data": {
"ok": true,
"kind": "deal",
"clientId": "hsbftmp3f5",
"leadId": null,
"dealId": "pq4r8tz2kd",
"pipelineId": "c7n3wv9e1s",
"stage": "Qualification",
"taskId": "m2x6b8yq0f"
}
}Name the organisation with clientId (a client's public id) or organisationName. If the organisation already has an open deal, the call answers 409 conflict with the existing deal in details.dealId; send "onExistingDeal": "create" to open another one anyway.
Import clients or contacts from a CSV
POST /v1/clients/actions/import-csv takes the CSV text and a mapping from Dashly fields to your CSV column headers:
curl -X POST https://api.usedashly.com/v1/clients/actions/import-csv \
-H "Authorization: Bearer $DASHLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "contacts",
"csv": "Company,First name,Last name,Email\nBoudbois,Claire,Martin,claire@boudbois.fr\nUnknown Co,Paul,,paul@example.com",
"mapping": {
"account": "Company",
"firstName": "First name",
"lastName": "Last name",
"email": "Email"
}
}'{
"data": {
"ok": true,
"imported": 1,
"skipped": 1,
"errors": [
{
"row": 3,
"code": "account_not_found",
"value": "Unknown Co",
"reason": "No client named \"Unknown Co\""
}
],
"warnings": []
}
}typeisclientsorcontacts. The first line of the CSV holds the headers.- Invalid rows are skipped, not fatal. Each is listed in
errors(up to 50) with its sheetrow(the header is row 1), a stablecode(required,too_long,invalid_email,invalid_value,account_not_found), the values that explain it (field,max,value) and an Englishreason. - For contacts, the
accountcolumn must match the exact name of an existing client. - A phone number that cannot be read is dropped rather than skipping the row; the row is listed in
warningswith the codephone_dropped. - The whole request body is limited to 1 MiB.
Merge contacts
POST /v1/contacts/{id}/actions/merge folds the given contacts into the one in the path: its empty fields are filled from theirs, everything that referenced them now references it, and they are deleted. This cannot be undone.
curl -X POST https://api.usedashly.com/v1/contacts/k7m2p9xq3n/actions/merge \
-H "Authorization: Bearer $DASHLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "contactIds": ["w3e8r1t6y2", "u9i4o7p0a5"] }'Read a client's site statistics
site-statistics is a read-only action on one client, so it answers GET with its parameters in the query string:
curl "https://api.usedashly.com/v1/clients/hsbftmp3f5/actions/site-statistics?days=28" \
-H "Authorization: Bearer $DASHLY_API_KEY"It needs clients:read, and returns the client's statistics from every connected source (Search Console, GA4 and others) over the period. Other read-only actions include GET /v1/clients/{id}/actions/ai-visibility, GET /v1/agency-settings/actions/overview?period=month and GET /v1/members/actions/my-day.
Set up a client's AI visibility
Competitors and brand aliases change without resending the whole list:
curl -X POST https://api.usedashly.com/v1/geo-settings/g7k2m9p4qx/actions/edit-competitors \
-H "Authorization: Bearer $DASHLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "add": ["Agence Nord"], "remove": ["Old Rival"] }'POST /v1/geo-settings/{id}/actions/capture runs a capture now, like the dashboard's button:
it spends the agency's allowance, so the key's owner must hold reports.manage and the module
must already be switched on for the client (switching it on is never possible with a key). It
answers { "status": "started" } at once. See AI visibility from Claude
for every setting.
Deprecated actions
An action scheduled for removal still works, and every successful answer carries a Deprecation: true header and a Sunset header with the removal date. The OpenAPI document marks it as deprecated and names what to use instead.
Cost and limits
An action that changes data costs 11 tokens and counts as a write for the rate limits; a read-only action is priced like a read. Actions accept an Idempotency-Key like any POST. See Rate limits and quota.