Idempotency
The tools that create a resource accept an optional idempotency_key argument. It is sent to the endpoint as the X-Idempotency-Key header the REST API already honors.
{ "name": "dashboards_portal_users_create",
"arguments": {
"email": "joao@example.com",
"tenant_id": "cliente-1042",
"idempotency_key": "7c4a8d09-ca37-4f21-b1f0-3d2f2b8c9a11"
} }
| Format | Opaque string, 1 to 255 characters. A UUID is the usual choice. |
| Retry with the same body | Returns the original response without running the operation again. |
| Retry with a different body | Conflict. |
Why an agent needs this more than your backend
An MCP client that times out and retries, or a model that decides "I'm not sure that worked, I'll do it again", is exactly the pattern that creates the second role, the second invitation and the second user. Your backend has a retry loop you wrote; the agent has judgment.
Send a key whenever the operation creates something or sends an invitation. Reads and updates are naturally idempotent and need no key.
Tools that accept it
| Family | Tools |
|---|---|
portal-users | dashboards_portal_users_create, dashboards_portal_users_invite |
users, roles | dashboards_dashboard_users_create, dashboards_dashboard_users_upsert, dashboards_team_roles_create, dashboards_team_roles_upsert |
:::caution Invitations
dashboards_portal_users_invite sends an email to the portal user — an effect in the world that no _delete undoes. Send idempotency_key with it even when you think you won't retry.
:::
Upsert is not idempotency
*_upsert matches an existing record by id, then external_id, then by the resource's business key, and creates only when nothing matches. That solves "I don't know if it already exists", a different problem from "I don't know if my call arrived". The two combine well: an upsert with an idempotency_key is safe on both counts.