| name | alpaca-broker-journals |
| description | Move cash (JNLC) and securities (JNLS) BETWEEN accounts inside your own Alpaca omnibus via the Broker API — single, batch, and reverse-batch journals, the Idempotency-Key header, journal status lifecycle including corrections, and the firm/sweep-account pattern that powers instant funding and share rewards. Use for internal account-to-account movement in any language. For deposits/withdrawals to EXTERNAL banks, use funding-transfers instead. |
Alpaca Broker API — Journals
Journals move value between two accounts within your own Alpaca omnibus — typically between a pre-funded firm/sweep account and a user account. They are the engine behind "instant funding," cashback, and share rewards. They never touch the outside banking world (that's alpaca-broker-funding-transfers).
Read alpaca-broker-integration first. Broker API + HTTP Basic auth.
Reference
- Guide:
https://docs.alpaca.markets/docs/funding-via-journals
- API ref:
https://docs.alpaca.markets/reference/createjournal
- Live schema:
alpaca-docs MCP → get-endpoint title "Broker API" path /v1/journals
1. Endpoints
| Method | Path | Purpose |
|---|
| POST | /v1/journals | Single journal (JNLC cash or JNLS shares) |
| POST | /v1/journals/batch | One source → many destinations (JNLC only) |
| POST | /v1/journals/reverse_batch | Many sources → one destination (JNLC only) |
| GET | /v1/journals | List (filters: after, before, status, entry_type, to_account, from_account, limit) |
| GET | /v1/journals/{journal_id} | Retrieve one |
| DELETE | /v1/journals/{journal_id} | Cancel a pending journal (204) |
| GET | /v2/events/journals/status | SSE journal status stream (v1 is legacy) |
2. JNLC vs JNLS
entry_type is exactly "JNLC" or "JNLS".
JNLC — cash. Moves USD between accounts. Allowed firm ↔ user, both directions. Not customer-to-customer.
JNLS — securities. Moves whole/fractional shares. Allowed firm → user only. Used for signup/referral share rewards.
{ "entry_type": "JNLC", "from_account": "<firm-uuid>", "to_account": "<user-uuid>", "amount": "100.00" }
{ "entry_type": "JNLS", "from_account": "<firm-uuid>", "to_account": "<user-uuid>", "symbol": "AAPL", "qty": "0.5" }
| Field | JNLC | JNLS | Notes |
|---|
from_account / to_account | required | required | account UUIDs |
amount | required | — | decimal string |
symbol / qty | — | required | qty is a string; fractional allowed |
currency | optional | optional | defaults USD |
description | optional | optional | ≤1024 chars; accepts sandbox fixtures |
transmitter_* | optional (JNLC) | n/a | Travel Rule fields |
Responses: 200 journal · 403 amount/assets not available · 404 account not found · 422 idempotency-key reused with a different body.
3. Idempotency-Key header — USE IT
Pass an Idempotency-Key header (≤128 chars; a client-generated UUID is recommended) on journal creates.
- Same key + identical body → returns the original journal (no duplicate).
- Same key + different body →
422.
Lesson: this is the correct way to make money movement retry-safe. Without it, a network timeout on POST /v1/journals leaves you unsure whether the cash moved — and a blind retry can double-fund. Generate the key deterministically from your own transaction ID and send it on every attempt.
4. Batch vs reverse-batch (JNLC only, all-or-nothing)
Batch — one-to-many (fan a sweep account out to many users):
{ "entry_type": "JNLC", "from_account": "<firm-uuid>",
"entries": [ { "to_account": "<u1>", "amount": "1000" }, { "to_account": "<u2>", "amount": "250" } ] }
Reverse batch — many-to-one (pull cash from many users back to the firm account):
{ "entry_type": "JNLC", "to_account": "<firm-uuid>",
"entries": [ { "from_account": "<u1>", "amount": "10" }, { "from_account": "<u2>", "amount": "100" } ] }
Every entry must validate or the entire batch fails (one bad account ID kills it). The response is an array of BatchJournalResponse (the Journal object + an error_message per entry that failed). Idempotency-Key is supported with the same semantics.
5. Status lifecycle
JournalStatus: queued, sent_to_clearing, pending, executed, rejected, canceled, refused, deleted, correct.
Happy path: queued → sent_to_clearing → executed.
| Status | Meaning | Terminal |
|---|
queued | In queue | no |
sent_to_clearing | Submitted to books-and-records | no |
pending | Needs Alpaca ops approval (e.g. hit a JNLC daily limit) | no |
executed | Balances updated — but NOT final, can still be reversed by cashiering | no (not final) |
rejected | Manually rejected | no |
refused | Failed preliminary checks; never hit the ledger (e.g. a fast replay failing the balance check) | no |
canceled | Canceled via API/ops | FINAL |
deleted | Removed from ledger | FINAL |
correct | A prior executed journal was cancelled and re-created with a corrected amount | FINAL |
Two critical lessons:
executed ≠ final. Don't treat executed as irreversible — Alpaca cashiering can reverse a journal that wasn't permitted. Reconcile against later events.
correct creates a NEW journal ID. A correction cancels the original and issues a new journal with the corrected amount — it is not an in-place edit. If you reconcile by journal ID, the original ID transitions to correct/cancelled while a different ID carries the real funds. Handle both. (This is why event consumers must be idempotent and ID-keyed — see alpaca-broker-reconciliation-idempotency.)
6. SSE journal events
GET /v2/events/journals/status pushes JournalStatusEventV2: event_id (ULID, sortable), journal_id, entry_type, status_from, status_to, description, idempotency_key, idempotency_key_type (single|batch), batch_error_message. Replay rules: since required if until set; since_id required if until_id set; can't mix since with since_id. Without a since/since_id, no history is returned. See alpaca-broker-sse-events.
7. Constraints & gotchas
- Eligibility: the cash-pooling/journals use case requires Alpaca review and possibly a local license — check with counsel.
- JNLS account states:
to_account must be ACTIVE; from_account must be ACTIVE or CLOSE.
- Sufficient funds: JNLC create →
403 if the amount isn't available; reverse-batch 403 = insufficient balance/assets.
- Daily limits push to
pending (manual ops approval).
GET /v1/journals returns 422 if the result set exceeds 100,000 records — always filter with after/before/limit.
- Delete is pending-only:
DELETE succeeds (204) only when pending; an executed journal → 422. To reverse an executed journal, create a mirror journal in the opposite direction, don't try to delete it.
- Travel Rule: include transmitter info on money-moving journals (required on all incoming deposits regardless of amount).
- Sandbox fixtures: put fixtures in
description (e.g. /fixtures/status=rejected/fixtures/) to simulate rejected/pending outcomes for testing.
8. The sweep-account funding pattern (why journals exist)
The canonical Broker API funding architecture:
Bulk external wire ──> FIRM / SWEEP account (pre-funded) ──JNLC──> user accounts (instant)
user account ──JNLC──> FIRM account ──external wire/ACH──> outside world (withdrawal)
You collect money your own way, hold it in a firm account, and journal it to users instantly rather than running a per-user external transfer. Withdrawals reverse the flow. This is what makes "instant deposit" UX possible on top of slow banking rails.
Related skills: external money in/out → alpaca-broker-funding-transfers; retry-safety & corrections → alpaca-broker-reconciliation-idempotency; decimal handling → alpaca-broker-money-precision; events → alpaca-broker-sse-events.