curl --request POST \
--url https://allocait.app/api/v1/portfolios/{id}/cash-movements \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"comment": "Opening balance",
"direction": "DEPOSIT",
"amount": 100000,
"idempotency_key": "opening-balance-2026-10-14"
}
'import requests
url = "https://allocait.app/api/v1/portfolios/{id}/cash-movements"
payload = {
"comment": "Opening balance",
"direction": "DEPOSIT",
"amount": 100000,
"idempotency_key": "opening-balance-2026-10-14"
}
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({
comment: 'Opening balance',
direction: 'DEPOSIT',
amount: 100000,
idempotency_key: 'opening-balance-2026-10-14'
})
};
fetch('https://allocait.app/api/v1/portfolios/{id}/cash-movements', 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/portfolios/{id}/cash-movements",
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([
'comment' => 'Opening balance',
'direction' => 'DEPOSIT',
'amount' => 100000,
'idempotency_key' => 'opening-balance-2026-10-14'
]),
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/portfolios/{id}/cash-movements"
payload := strings.NewReader("{\n \"comment\": \"Opening balance\",\n \"direction\": \"DEPOSIT\",\n \"amount\": 100000,\n \"idempotency_key\": \"opening-balance-2026-10-14\"\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/portfolios/{id}/cash-movements")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"comment\": \"Opening balance\",\n \"direction\": \"DEPOSIT\",\n \"amount\": 100000,\n \"idempotency_key\": \"opening-balance-2026-10-14\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://allocait.app/api/v1/portfolios/{id}/cash-movements")
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 \"comment\": \"Opening balance\",\n \"direction\": \"DEPOSIT\",\n \"amount\": 100000,\n \"idempotency_key\": \"opening-balance-2026-10-14\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"action_id": "47857a5d-d83d-4fb9-8c2b-46103bcee205",
"cash_balance": 100000
}{
"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"
}Deposit or withdraw cash from a portfolio
Recorded as an action with symbol: "CASH"; GET /actions lists it.
The response names the action and the new cash balance only.
A withdrawal larger than the cash balance is refused with 409 and
error_code: INSUFFICIENT_CASH. The check runs on the server under a
lock. This records the change in Allocait only. It moves no money and
places no order.
Pass idempotency_key to make a retry safe: calling again with the
same key and the same direction/amount/comment returns the
original result (idempotent_replay: true) instead of applying the
movement twice. The same key with a different body is refused with
409 and error_code: IDEMPOTENCY_KEY_CONFLICT. Omit it for a call
you know will only ever be made once.
Requires the API key to have the write permission.
curl --request POST \
--url https://allocait.app/api/v1/portfolios/{id}/cash-movements \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"comment": "Opening balance",
"direction": "DEPOSIT",
"amount": 100000,
"idempotency_key": "opening-balance-2026-10-14"
}
'import requests
url = "https://allocait.app/api/v1/portfolios/{id}/cash-movements"
payload = {
"comment": "Opening balance",
"direction": "DEPOSIT",
"amount": 100000,
"idempotency_key": "opening-balance-2026-10-14"
}
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({
comment: 'Opening balance',
direction: 'DEPOSIT',
amount: 100000,
idempotency_key: 'opening-balance-2026-10-14'
})
};
fetch('https://allocait.app/api/v1/portfolios/{id}/cash-movements', 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/portfolios/{id}/cash-movements",
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([
'comment' => 'Opening balance',
'direction' => 'DEPOSIT',
'amount' => 100000,
'idempotency_key' => 'opening-balance-2026-10-14'
]),
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/portfolios/{id}/cash-movements"
payload := strings.NewReader("{\n \"comment\": \"Opening balance\",\n \"direction\": \"DEPOSIT\",\n \"amount\": 100000,\n \"idempotency_key\": \"opening-balance-2026-10-14\"\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/portfolios/{id}/cash-movements")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"comment\": \"Opening balance\",\n \"direction\": \"DEPOSIT\",\n \"amount\": 100000,\n \"idempotency_key\": \"opening-balance-2026-10-14\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://allocait.app/api/v1/portfolios/{id}/cash-movements")
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 \"comment\": \"Opening balance\",\n \"direction\": \"DEPOSIT\",\n \"amount\": 100000,\n \"idempotency_key\": \"opening-balance-2026-10-14\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"action_id": "47857a5d-d83d-4fb9-8c2b-46103bcee205",
"cash_balance": 100000
}{
"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
portfolio_id
Body
DEPOSIT, WITHDRAW Must be positive.
A unique token identifying this exact request. Reusing it with the same direction/amount/comment returns the original result rather than applying it again; reusing it with a different body is refused (409). Optional. An omitted key gets no deduplication.
200Response
The movement was recorded
The portfolio's cash balance after the movement.
Present and true when this response is the original result of an earlier call with the same idempotency_key, not a fresh application of the movement. Absent on a normal (non-replayed) call.