Dashly Docs

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

TargetPathExample
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": []
  }
}
  • type is clients or contacts. 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 sheet row (the header is row 1), a stable code (required, too_long, invalid_email, invalid_value, account_not_found), the values that explain it (field, max, value) and an English reason.
  • For contacts, the account column 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 warnings with the code phone_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.

On this page