curl --request POST \
--url https://allocait.app/api/v1/positions/{symbol} \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"shares": 123,
"price": 1,
"asset_class": "<string>",
"mark": 500000000,
"comment": "<string>"
}
'import requests
url = "https://allocait.app/api/v1/positions/{symbol}"
payload = {
"shares": 123,
"price": 1,
"asset_class": "<string>",
"mark": 500000000,
"comment": "<string>"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
shares: 123,
price: 1,
asset_class: '<string>',
mark: 500000000,
comment: '<string>'
})
};
fetch('https://allocait.app/api/v1/positions/{symbol}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://allocait.app/api/v1/positions/{symbol}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'shares' => 123,
'price' => 1,
'asset_class' => '<string>',
'mark' => 500000000,
'comment' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://allocait.app/api/v1/positions/{symbol}"
payload := strings.NewReader("{\n \"shares\": 123,\n \"price\": 1,\n \"asset_class\": \"<string>\",\n \"mark\": 500000000,\n \"comment\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://allocait.app/api/v1/positions/{symbol}")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"shares\": 123,\n \"price\": 1,\n \"asset_class\": \"<string>\",\n \"mark\": 500000000,\n \"comment\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://allocait.app/api/v1/positions/{symbol}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"shares\": 123,\n \"price\": 1,\n \"asset_class\": \"<string>\",\n \"mark\": 500000000,\n \"comment\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"portfolio_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"created": [
{}
],
"updated": [
{}
],
"closed": [
{}
],
"unchanged": [
{}
],
"action_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"unclassified": [
"<string>"
]
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Internal error",
"error_code": "INTERNAL_ERROR",
"request_id": "5b1e7c52-3f4a-4d0e-9a55-2d5cbe6f0a11"
}Record one holding's new total shares
Sets one holding to its new TOTAL share count and leaves every other
holding alone. It opens a holding you don’t own yet, and shares: 0
closes one. The cash difference settles atomically against the
portfolio’s cash position: buying debits cash, selling credits it,
and insufficient cash is rejected (INSUFFICIENT_CASH). Backed by
the same function as /portfolios/{id}/positions/apply and the MCP
server’s save_position, so a repeat with the same total changes
nothing. CASH is refused; record cash with
/portfolios/{id}/cash-movements.
This records the change in Allocait only. It moves no money and places no order.
Shorts. A negative shares is a short, and it works only on a
portfolio with allow_shorts on (PATCH /portfolios/{id}) and never
for a holding in the Options class; otherwise the call is
INVALID_INPUT (Share count cannot be negative while Allow shorts is off., or Options are held long only, so a count cannot be negative.), carries symbols, and writes nothing. A short sale
credits cash and closing one (shares: 0) debits it, at the price
you send, so a cover that cash cannot pay is INSUFFICIENT_CASH.
Going from long to short in one call, or the reverse, is one change
at that price. A change worth 100000000000000 or more is refused
(That trade is too large to record.). Allocait records the short;
it computes no borrow, margin or profit on it.
Moves. moves is how many times its index a fund moves: 1, 2,
3, -1, -2 or -3 (negative is an inverse fund). Left out, the stored
value stays (1 for a new holding), exactly like asset_class.
Allocait never guesses it from a symbol, so send it only when the
person says the fund is inverse or leveraged. A change of moves
alone is not a trade: it needs no price, moves no cash and records
no action. It applies to stocks and funds, not to cash or a holding
in the Options class (INVALID_INPUT: Moves applies to stocks and funds, not to cash or options.). Exposure reads it (ADR-142).
Options and marks. A holding in the Options class is recorded as
contracts × 100 at the per-share premium, and its price is its mark,
the premium the person recorded. Send mark to set it. Left out, a
change of the share count sets the mark to the price you send, and a
call that changes neither leaves it alone. A change of mark alone is
not a trade: no price, no cash, no action. A mark on a holding that
ends outside the Options class is INVALID_INPUT (A mark applies to options only.), and one above 1000000000 is refused. mark_on is the
day the mark was set or changed; the same mark again keeps it. An
option counts at its mark, with no price from you needed, and counts 0
from the day after the expiry in its symbol (US Eastern). Allocait
changes nothing on an expired option and cannot know what became of
it; close it with shares: 0 and price: 0 once the person says
(ADR-147).
Symbols. The symbol is trimmed and uppercased, so alfa and
ALFA are the same holding. After that it must be a ticker of 1 to
21 characters of A-Z, 0-9, . or - starting with a letter or digit
(ALFA, ALFA.B, or an option symbol such as ECHO281215P00050000),
or a slash pair (ALFA/BRAV). Anything else is a 400 with “Each
position needs a valid symbol: a ticker (ALFA, ALFA.B) or a pair
(ALFA/BRAV)”.
Requires the API key to have the write permission. Keys are
created with a permission list; a key without write receives 403
on this endpoint even with a valid subscription.
After updating, call /compliance/check with your own prices to see the new state.
curl --request POST \
--url https://allocait.app/api/v1/positions/{symbol} \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"shares": 123,
"price": 1,
"asset_class": "<string>",
"mark": 500000000,
"comment": "<string>"
}
'import requests
url = "https://allocait.app/api/v1/positions/{symbol}"
payload = {
"shares": 123,
"price": 1,
"asset_class": "<string>",
"mark": 500000000,
"comment": "<string>"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
shares: 123,
price: 1,
asset_class: '<string>',
mark: 500000000,
comment: '<string>'
})
};
fetch('https://allocait.app/api/v1/positions/{symbol}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://allocait.app/api/v1/positions/{symbol}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'shares' => 123,
'price' => 1,
'asset_class' => '<string>',
'mark' => 500000000,
'comment' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://allocait.app/api/v1/positions/{symbol}"
payload := strings.NewReader("{\n \"shares\": 123,\n \"price\": 1,\n \"asset_class\": \"<string>\",\n \"mark\": 500000000,\n \"comment\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://allocait.app/api/v1/positions/{symbol}")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"shares\": 123,\n \"price\": 1,\n \"asset_class\": \"<string>\",\n \"mark\": 500000000,\n \"comment\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://allocait.app/api/v1/positions/{symbol}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"shares\": 123,\n \"price\": 1,\n \"asset_class\": \"<string>\",\n \"mark\": 500000000,\n \"comment\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"portfolio_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"created": [
{}
],
"updated": [
{}
],
"closed": [
{}
],
"unchanged": [
{}
],
"action_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"unclassified": [
"<string>"
]
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Portfolio not found or access denied",
"error_code": "NOT_FOUND"
}{
"error": "Internal error",
"error_code": "INTERNAL_ERROR",
"request_id": "5b1e7c52-3f4a-4d0e-9a55-2d5cbe6f0a11"
}Authorizations
Raw API key shown once when created in the Allocait app (Settings > API keys). Send it verbatim in this header; the server stores and compares only its SHA-256 hash. Allocait generates every key: alc_ followed by 64 lowercase hex characters. Keys issued before 2026-10-10 are 64 hex characters with no prefix and keep working. A key may carry an expiry (90 days or 1 year, or never). A malformed, unknown, revoked or expired key gets 401 with no further detail, and a malformed one is refused before any lookup. Keys cannot be created through this API.
Path Parameters
"ALFA"
Query Parameters
Body
New TOTAL share count (0 to close without deleting). Negative is a short, recorded only on a portfolio with allow_shorts on and never in the Options class (ADR-153). For a holding filed under the Options system asset class, recorded as contracts × 100 at the per-share premium (ADR-056).
Price per share used to compute the cash difference. Required when the share count changes.
x >= 0Class name, rules text alias or id to file the holding under. A new holding without one is left unclassified.
Times its index the fund moves; negative is inverse. Left out, the stored value stays (1 for a new holding). Not for cash or an Options-class holding (ADR-142). A moves-only change needs no price.
1, 2, 3, -1, -2, -3 An option's per-share premium. Only for a holding in the Options class (INVALID_INPUT otherwise). Left out, a change of the share count sets it to price, and otherwise the stored mark stays. A mark-only change needs no price and moves no cash (ADR-147).
0 <= x <= 1000000000The holding's sizing mode, as set in the app's position wizard.
MANUAL, MINIMUM, BALANCED, MAXIMUM Why, recorded on the action.
Response
The holding was recorded
Share changes, plus class, sizing and Moves changes on an unchanged
share count as {symbol, shares, asset_class_id, sizing}.
Holdings taken to 0 shares. Closing a short is a cover, which debits cash; read the sign of the change in shares, not the word, to tell which way cash moved.
Symbols already matching the desired state, shares and class. No RPC call was made for these.
The actions this call recorded, for PATCH /actions/{id} to add a comment later.
Holdings left with no asset class. No band judges them until they have one.