English
English
English
English
Two endpoints for writing goals into SalesDash from wherever you plan them — a spreadsheet, an HR system, your own planning tool.
POST /goal creates or updates a goal for one or more teams.POST /agent-goal sets one agent's personal goal for a week, month, quarter or year.Both are upserts: send the same goal again and you update what's there instead of creating a second copy. That makes them safe to run on a schedule.
Both refuse a metric that has no per-agent number, with a 422 — the same metrics metric-scores refuses. Filtering your metric list on can_rank: true avoids nearly all of those. A few kinds of metric cannot carry a goal at all whatever their ranking says — a tiered metric, for instance, where a target sits between two tiers and means nothing — and those say so in the 422.
value is a bare number, and SalesDash accepts whatever you send. Send the wrong unit and nothing complains — the goal simply sits there at a hundredth of what you meant, or a thousand times more, until someone notices the progress bar is absurd.
The two endpoints do not agree with each other on money. Check this table against the metric's display_mode from the metric list before you send anything:
display_mode | POST /goal (team) | POST /agent-goal (personal) |
|---|---|---|
currency | Euros — 50000 is €50.000 | Cents — 5000000 is €50.000 |
duration | Seconds — 7200 is two hours | Seconds — 7200 is two hours |
percentage | A fraction — 0.4 is 40% | A fraction — 0.4 is 40% |
number, score, ratio, raw | The number as the metric reports it | The number as the metric reports it |
Time lines up everywhere: a duration goal is in seconds, the same unit metric-scores reports, so a figure carries straight across without conversion. Money does not — a currency goal is in euros for a team and in cents for a person.
Check a goal after you first write it
Open the goal in the admin, or on the agent's own profile, and confirm the figure reads the way you meant it. Getting this wrong is the most common way a goal integration goes quietly wrong, and the difference between €500 and €50.000 is invisible in the request itself.
POST /integrations/api/v1/goal
Content-Type: application/json{
"name": "Q4 contract value", // Required
"metric_id": 2, // Required
"active_from": "2026-10-01", // Required
"active_to": "2026-12-31", // Required, on or after active_from
"team_goals": [ // Required, at least one team
{ "team_id": 2, "value": 500000 },
{ "team_id": 3, "value": 350000 }
]
}{
"message": "Goal created successfully.",
"goal_id": 3
}You get 201 when the goal was created and 200 when an existing one was updated, and the message says which.
The name is the identity. SalesDash matches on name across all your goals, so re-sending the same name updates that goal — its window, its metric and its per-team values all move to what you just sent. Two goals you want to keep apart need two different names, and a name you reuse for something else overwrites the old goal rather than adding one.
A value of 0 takes that team off the goal, the same as clearing the field in the admin. That is how you drop a team without deleting the goal itself.
Averages and ratios can only carry one team
A metric that averages or divides — average contract value, conversion rate — cannot be split across teams, because the results don't add up into a whole. Send more than one team with a value above zero for such a metric and you get a 422. Give it a single team, or make a goal per team.
The goal appears under Goals in the admin, where you can add the parts this endpoint deliberately leaves alone — coin rewards and successor goals.
POST /integrations/api/v1/agent-goal
Content-Type: application/json{
"agent_id": 7, // Required
"metric_id": 2, // Required
"value": 5000000, // Required — mind the unit table above
"month": "2026-09" // Required: exactly one of week, month, quarter, year
}Name exactly one period. Naming none, or naming two, is a 422 — a goal with no period is one nobody can date and nobody can find again.
| Period | Send | Comes back as |
|---|---|---|
week | A date in the week — 2026-09-16 | The Monday of that week — 2026-09-14 |
month | 2026-09 | 2026-09 |
quarter | A month in the quarter — 2026-09 | The quarter's first month — 2026-07 |
year | 2026 | 2026 |
Any date inside the period works: send 2026-09-14 as a month and it lands on September, exactly as if you had sent 2026-09. So you can pass a real date straight from your own system without working out which week or quarter it belongs to — but read the value back, because it will not be the string you sent.
The response is the stored goal:
{
"data": {
"unit": "euro",
"display_mode": "currency",
"year": null,
"quarter": null,
"month": "2026-09",
"week": null,
"value": 5000000,
"is_locked": false
}
}Read month/quarter/week/year back to confirm which period you actually landed on, and value to confirm the unit came out the way you meant.
One personal goal per agent per period
An agent has one goal per period, not one per metric. Setting a September goal replaces whatever was there — including a goal on a different metric. Read the response back if that matters to you.
The one-hour edit lock does not apply here. In the app an agent can only change their own goal within an hour of setting it, so results can't be back-fitted. An API key belongs to an admin, so this endpoint writes regardless — which is what you want for a scheduled sync, and worth knowing if agents also set their own goals.
actual and is_locked come back as part of the goal but are meant for the app's own screens; actual is not filled in here. To see how an agent is doing, ask metric-scores for the same metric over the same dates.