curl --request POST \
--url https://allocait.app/api/v1/positions/size \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"current_shares": 123,
"portfolio_value": 123,
"price": 123,
"current_price": 123,
"size_min_pct": 50,
"size_max_pct": 50,
"min_position_pct": 50,
"max_position_pct": 50,
"sizing_mode": "MANUAL",
"basis_points": 0,
"manual_shares": 123,
"direction": "long",
"cash": 0,
"current_cash": 123
}
'import requests
url = "https://allocait.app/api/v1/positions/size"
payload = {
"current_shares": 123,
"portfolio_value": 123,
"price": 123,
"current_price": 123,
"size_min_pct": 50,
"size_max_pct": 50,
"min_position_pct": 50,
"max_position_pct": 50,
"sizing_mode": "MANUAL",
"basis_points": 0,
"manual_shares": 123,
"direction": "long",
"cash": 0,
"current_cash": 123
}
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({
current_shares: 123,
portfolio_value: 123,
price: 123,
current_price: 123,
size_min_pct: 50,
size_max_pct: 50,
min_position_pct: 50,
max_position_pct: 50,
sizing_mode: 'MANUAL',
basis_points: 0,
manual_shares: 123,
direction: 'long',
cash: 0,
current_cash: 123
})
};
fetch('https://allocait.app/api/v1/positions/size', 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/size",
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([
'current_shares' => 123,
'portfolio_value' => 123,
'price' => 123,
'current_price' => 123,
'size_min_pct' => 50,
'size_max_pct' => 50,
'min_position_pct' => 50,
'max_position_pct' => 50,
'sizing_mode' => 'MANUAL',
'basis_points' => 0,
'manual_shares' => 123,
'direction' => 'long',
'cash' => 0,
'current_cash' => 123
]),
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/size"
payload := strings.NewReader("{\n \"current_shares\": 123,\n \"portfolio_value\": 123,\n \"price\": 123,\n \"current_price\": 123,\n \"size_min_pct\": 50,\n \"size_max_pct\": 50,\n \"min_position_pct\": 50,\n \"max_position_pct\": 50,\n \"sizing_mode\": \"MANUAL\",\n \"basis_points\": 0,\n \"manual_shares\": 123,\n \"direction\": \"long\",\n \"cash\": 0,\n \"current_cash\": 123\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/size")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"current_shares\": 123,\n \"portfolio_value\": 123,\n \"price\": 123,\n \"current_price\": 123,\n \"size_min_pct\": 50,\n \"size_max_pct\": 50,\n \"min_position_pct\": 50,\n \"max_position_pct\": 50,\n \"sizing_mode\": \"MANUAL\",\n \"basis_points\": 0,\n \"manual_shares\": 123,\n \"direction\": \"long\",\n \"cash\": 0,\n \"current_cash\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://allocait.app/api/v1/positions/size")
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 \"current_shares\": 123,\n \"portfolio_value\": 123,\n \"price\": 123,\n \"current_price\": 123,\n \"size_min_pct\": 50,\n \"size_max_pct\": 50,\n \"min_position_pct\": 50,\n \"max_position_pct\": 50,\n \"sizing_mode\": \"MANUAL\",\n \"basis_points\": 0,\n \"manual_shares\": 123,\n \"direction\": \"long\",\n \"cash\": 0,\n \"current_cash\": 123\n}"
response = http.request(request)
puts response.read_body{
"target_shares": 123,
"target_value": 123,
"target_pct": 123,
"current_value": 123,
"current_pct": 123,
"shares_delta": 123,
"value_delta": 123,
"cash_impact": 123,
"cash_after_trade": 123,
"can_afford": true,
"compliance_status": "COMPLIANT"
}{
"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"
}Calculate a target share count for a position
Pure calculation, no portfolio ownership involved: pass in current
holding, price, portfolio value, and a sizing mode and get back the
target share count, its cash impact, and whether it is affordable.
MINIMUM, BALANCED and MAXIMUM size one holding to the bottom,
midpoint or top of the position size range you pass as
size_min_pct / size_max_pct (the rule’s size_* fields from
GET /position-rules), not to the asset class allocation band.
Nothing is written; call /portfolios/:id/positions/apply to act on
the result.
sizing_mode is MANUAL unless you send another. MANUAL with a
manual_shares sizes to that count. MANUAL without one starts
from the size held now (current_shares), so basis_points moves
the holding by that many basis points of portfolio_value, up or
down. basis_points is also added to the target of MINIMUM,
BALANCED and MAXIMUM. The target is held between 0 and 100
percent of portfolio_value, not inside the size range: when it
lands outside, compliance_status is BELOW_MIN or ABOVE_MAX
and the share count is still the one asked for.
direction is long (the default) or short. For a short, pass
the short size range of the asset class as size_min_pct /
size_max_pct (the rule’s short_size_* fields from GET /position-rules), because this calculation reads no rule. Pass a
negative current_shares for a short you hold and a positive
manual_shares. The share count, value and percent come back
negative for a short, and compliance_status compares the size
without its sign with the range.
min_position_pct / max_position_pct are deprecated aliases for
size_min_pct / size_max_pct and mean the same thing here. When
both spellings are sent, size_* wins. price and cash are the
names MCP’s calculate_position_size uses; current_price and
current_cash are older names for the same fields, read only when
the new name is absent.
curl --request POST \
--url https://allocait.app/api/v1/positions/size \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"current_shares": 123,
"portfolio_value": 123,
"price": 123,
"current_price": 123,
"size_min_pct": 50,
"size_max_pct": 50,
"min_position_pct": 50,
"max_position_pct": 50,
"sizing_mode": "MANUAL",
"basis_points": 0,
"manual_shares": 123,
"direction": "long",
"cash": 0,
"current_cash": 123
}
'import requests
url = "https://allocait.app/api/v1/positions/size"
payload = {
"current_shares": 123,
"portfolio_value": 123,
"price": 123,
"current_price": 123,
"size_min_pct": 50,
"size_max_pct": 50,
"min_position_pct": 50,
"max_position_pct": 50,
"sizing_mode": "MANUAL",
"basis_points": 0,
"manual_shares": 123,
"direction": "long",
"cash": 0,
"current_cash": 123
}
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({
current_shares: 123,
portfolio_value: 123,
price: 123,
current_price: 123,
size_min_pct: 50,
size_max_pct: 50,
min_position_pct: 50,
max_position_pct: 50,
sizing_mode: 'MANUAL',
basis_points: 0,
manual_shares: 123,
direction: 'long',
cash: 0,
current_cash: 123
})
};
fetch('https://allocait.app/api/v1/positions/size', 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/size",
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([
'current_shares' => 123,
'portfolio_value' => 123,
'price' => 123,
'current_price' => 123,
'size_min_pct' => 50,
'size_max_pct' => 50,
'min_position_pct' => 50,
'max_position_pct' => 50,
'sizing_mode' => 'MANUAL',
'basis_points' => 0,
'manual_shares' => 123,
'direction' => 'long',
'cash' => 0,
'current_cash' => 123
]),
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/size"
payload := strings.NewReader("{\n \"current_shares\": 123,\n \"portfolio_value\": 123,\n \"price\": 123,\n \"current_price\": 123,\n \"size_min_pct\": 50,\n \"size_max_pct\": 50,\n \"min_position_pct\": 50,\n \"max_position_pct\": 50,\n \"sizing_mode\": \"MANUAL\",\n \"basis_points\": 0,\n \"manual_shares\": 123,\n \"direction\": \"long\",\n \"cash\": 0,\n \"current_cash\": 123\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/size")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"current_shares\": 123,\n \"portfolio_value\": 123,\n \"price\": 123,\n \"current_price\": 123,\n \"size_min_pct\": 50,\n \"size_max_pct\": 50,\n \"min_position_pct\": 50,\n \"max_position_pct\": 50,\n \"sizing_mode\": \"MANUAL\",\n \"basis_points\": 0,\n \"manual_shares\": 123,\n \"direction\": \"long\",\n \"cash\": 0,\n \"current_cash\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://allocait.app/api/v1/positions/size")
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 \"current_shares\": 123,\n \"portfolio_value\": 123,\n \"price\": 123,\n \"current_price\": 123,\n \"size_min_pct\": 50,\n \"size_max_pct\": 50,\n \"min_position_pct\": 50,\n \"max_position_pct\": 50,\n \"sizing_mode\": \"MANUAL\",\n \"basis_points\": 0,\n \"manual_shares\": 123,\n \"direction\": \"long\",\n \"cash\": 0,\n \"current_cash\": 123\n}"
response = http.request(request)
puts response.read_body{
"target_shares": 123,
"target_value": 123,
"target_pct": 123,
"current_value": 123,
"current_pct": 123,
"shares_delta": 123,
"value_delta": 123,
"cash_impact": 123,
"cash_after_trade": 123,
"can_afford": true,
"compliance_status": "COMPLIANT"
}{
"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.
Body
Shares held now. Negative for a short you hold.
The portfolio's total value, above 0.
Per-share price, above 0. Required, as price or current_price.
Older name for price. Read only when price is absent.
Lower bound of the position size range for this one holding, as % of portfolio value. Omit both size bounds for MANUAL sizing; the response is then UNEVALUATED.
0 <= x <= 100Upper bound of the position size range for this one holding, as % of portfolio value.
0 <= x <= 100Deprecated alias for size_min_pct. Read only when size_min_pct is absent or null.
0 <= x <= 100Deprecated alias for size_max_pct. Read only when size_max_pct is absent or null.
0 <= x <= 100MANUAL (the default) sizes to manual_shares, or starts from the current size when there is none. MINIMUM, BALANCED and MAXIMUM size to the bottom, midpoint or top of the size range, and each needs its bound: MINIMUM needs size_min_pct, MAXIMUM needs size_max_pct, BALANCED needs both (a mode without its bound is refused with 400).
MANUAL, MINIMUM, BALANCED, MAXIMUM Basis points added to the target percent, either sign (100 = 1%). With MANUAL and no manual_shares the target is the current size, so this moves the holding. A positive adjustment makes a short larger. Ignored when manual_shares is given (a typed count is the size, so nothing is added to it).
Target shares for MANUAL. Always a positive count, a short's too; direction sets the side.
long (the default) or short. null counts as not sent. For a short, send the class's short size range as size_min_pct and size_max_pct; the count comes back negative.
long, short, null Cash available. Without it (or current_cash), the response leaves out can_afford and cash_after_trade.
Older name for cash. Read only when cash is absent.
Response
Sizing result
For direction short, target_shares, target_value,
target_pct, current_value, current_pct, shares_delta and
value_delta are negative, and compliance_status judges the size
without its sign. A portfolio worth zero or less is sized to 0 shares
and 0%, read against the range, as it always was.
target_shares minus the shares you supplied
Cash debited (negative) or credited (positive) if this size were applied
Present only when the request sent cash (or current_cash).
Present only when the request sent cash (or current_cash).
Verdict of target_pct, without its sign, against the range you
supplied (size_min_pct / size_max_pct). UNEVALUATED when
either bound was omitted; it is not a pass. The target is never
moved into the range to make it pass: a basis_points
adjustment that lands outside it reads BELOW_MIN or ABOVE_MAX.
This is not the asset class verdict from /compliance/check.
COMPLIANT, BELOW_MIN, ABOVE_MAX, UNEVALUATED