Account profile — API v1 and MCP | Nodea Documentation
The Account profile module covers reading and partially updating the token user's profile: identity, company and billing address, the VAT profile (country, company status, VAT number with VIES validation), tax number and timezone. This document describes the REST API v1 layer (/api/v1/profile) and the MCP tools profile-get and profile-update.
Authentication and scopes
API v1 authenticates with a Passport personal access token via the Authorization: Bearer <token> header. The profile operates exclusively on the token user — it accepts no ids, so there is no cross-tenant surface.
account:read— read the profile (GET /profile).account:write— partial update (PATCH /profile).
The profile is not gated by any Pennant flag — account access is never feature-flagged (same as the web UI). Rate limit: throttle:api-v1 = 240/min per token user.
Deliberately omitted (web-only)
Sensitive, confirmation-bound operations stay in the web UI and are not exposed over API v1:
- email change,
- password change,
- 2FA (two-factor) management,
- OAuth provider disconnect,
- avatar upload,
- billing currency change (locked in the web by the zero-wallet-balance guard).
The email field is returned on read, but PATCH /profile has no rule for it — submitting it is ignored (the field is not part of validated()). By contrast the MCP profile-update tool explicitly rejects email and password with a validation error (see the MCP section) and — unlike the API — can change the currency.
API v1 — endpoints
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/profile | account:read | The token user's profile (details, company, address, VAT, timezone, currency). |
| PATCH | /api/v1/profile | account:write | Partial update of any subset of general and VAT fields. Returns the updated profile. |
GET /profile — response
Returns a ProfileResource — fields: id, name, email, company_name, address, country, is_company (bool), vat_number, vat_validated (bool), vat_validated_at (ISO 8601 or null), tax_number, timezone, currency (PLN|EUR), created_at. It never returns secrets, 2FA state, password/remember hashes, OAuth provider rows or admin flags.
PATCH /profile — body parameters
Every rule carries sometimes — a client may send any subset of fields; omitted fields are left untouched (partial update).
name(string,sometimes|required, max 255) — a submitted name may not be blank.company_name(string|null, max 255).address(string|null, max 1000).tax_number(string|null, max 64).timezone(string|null,timezonerule — IANA identifier).country(string|null,size:2— ISO 3166-1 alpha-2).is_company(boolean).vat_number(string|null, max 32).
Currency is not accepted by the API — changing currency is web-only (and available in MCP). General fields (name, company_name, address, tax_number, timezone) are persisted only when present in the payload.
VAT profile and VIES re-validation
Submitting any of country / is_company / vat_number invalidates the previous VIES validation: vat_validated is set to false and vat_validated_at to null. Then, when the effective profile (submitted values merged over stored ones) is a company (is_company = true) with both a country and a VAT number set, a round-trip to VIES (ViesValidator) is performed. The country and VAT number are normalised (upper-cased, trimmed). Like the web flow, a failed VIES lookup still persists the submitted values — vat_validated simply stays false (no reverse-charge branch). VIES uses SOAP (not the Http facade) and is sandboxed by default outside production (billing.vies.sandbox).
Error codes
401— missing/invalid token.403— missing required scope (account:readfor GET,account:writefor PATCH).422— validation (e.g. blankname,countrynotsize:2, invalid timezone).
Examples — API
Read the profile
curl -s https://app.nodea.io/api/v1/profile \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Accept: application/json"
Update general details and timezone
curl -s -X PATCH https://app.nodea.io/api/v1/profile \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "John Smith",
"company_name": "Smith Ltd.",
"address": "1 Example St, London",
"timezone": "Europe/London"
}'
Set the VAT profile (triggers VIES re-validation)
curl -s -X PATCH https://app.nodea.io/api/v1/profile \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"is_company": true,
"country": "PL",
"vat_number": "PL1234567890"
}'
MCP — tools
MCP tools share the auth and scope model with API v1 (Bearer + scope checked in ensureScope). The profile is ungated, so both tools are always present in tools/list. They operate on the token user only.
| Tool | Scope | Type | Description |
|---|---|---|---|
| profile-get | account:read | read-only | Returns the profile: name, email, company_name, address, country, company/VAT status, tax_number, timezone and currency. No credentials or 2FA state. |
| profile-update | account:write | idempotent | Partial update; only provided fields change. Additionally supports currency. Rejects email/password. |
profile-get
No parameters. The response is a superset of ProfileResource — the same fields plus effective_timezone (the user's resolved timezone when timezone is null).
profile-update — parameters
All optional; only provided fields change. Empty strings / null clear the field (blank → null), country and VAT number are upper-cased.
name(string, max 255).company_name(string, max 255; null/empty clears).address(string, max 1000; null/empty clears).tax_number(string, max 64; null/empty clears).timezone(string — IANA identifier; null clears).country(string — ISO 3166-1 alpha-2; null clears).is_company(boolean).vat_number(string, max 32; null/empty clears).currency(enumPLN|EUR) — locked while any wallet holds a non-zero balance (returns a tool error); the change is written to the activity log.
Differences from the API: changing a VAT field (country/is_company/vat_number) to a value different from the stored one resets vat_validated (and vat_validated_at), but this tool does not perform the VIES round-trip — re-validation is done in the web UI. Submitting email or password fails validation (the prohibited rule: "The email address / password can only be changed in the web UI.") rather than being silently ignored as in the API.
Example — MCP (JSON-RPC)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "profile-get",
"arguments": {}
}
}
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "profile-update",
"arguments": {
"is_company": true,
"country": "PL",
"vat_number": "PL1234567890",
"timezone": "Europe/Warsaw"
}
}
}