API Overview

The Argo Books API lets your application send accounting data into a merchant's books. It lives at https://argorobots.com/v1.

The One Thing to Understand First

Argo Books is desktop software. A merchant's books live in a file on their own machine, not on our servers. So this API is an inbound queue, not a copy of their accounts.

When you create an object here, it is a proposal. It waits until the merchant opens Argo Books, reviews what you sent, and imports it. Three things follow from that, and they will save you time if you design around them from the start:

  • Every object has an import.status of pending, imported, or rejected. Poll it to find out what happened to your data.
  • Our ids are not their ids. After an import, import.local_ref holds the id the merchant's copy of Argo Books assigned.
  • An object freezes once it is imported. Update and delete return 409 object_not_pending. The merchant already has a copy, so changing the original here would leave two versions of one fact. To correct something after the fact, push a correcting object.

There is no fixed timeline for step two. A merchant who opens Argo Books weekly will import your data weekly. GET /v1/account reports how much of yours is still waiting.

Getting Started

Ask the merchant for a key. They create one in Argo Books under Settings, then Integrations, then Argo Books API. Keys start with ab_. See Authentication.

Confirm it works:

curl https://argorobots.com/v1/account \
  -H "Authorization: Bearer ab_..."
$ch = curl_init('https://argorobots.com/v1/account');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ab_...'],
]);

$account = json_decode(curl_exec($ch), true);
echo $account['id'];
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "ab_...");

var account = await http.GetFromJsonAsync<JsonElement>(
    "https://argorobots.com/v1/account");

Console.WriteLine(account.GetProperty("id").GetString());
const res = await fetch("https://argorobots.com/v1/account", {
  headers: { Authorization: "Bearer ab_..." },
});

const account = await res.json();
console.log(account.id);
import requests

account = requests.get(
    "https://argorobots.com/v1/account",
    headers={"Authorization": "Bearer ab_..."},
).json()

print(account["id"])

Then send something:

curl https://argorobots.com/v1/revenue \
  -H "Authorization: Bearer ab_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
    "description": "Order #1042",
    "amount": 11300,
    "currency": "usd",
    "tax_amount": 1300,
    "occurred_on": "2026-08-14",
    "reference": "1042"
  }'
$payload = json_encode([
    'description' => 'Order #1042',
    'amount'      => 11300,
    'currency'    => 'usd',
    'tax_amount'  => 1300,
    'occurred_on' => '2026-08-14',
    'reference'   => '1042',
]);

$ch = curl_init('https://argorobots.com/v1/revenue');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ab_...',
        'Content-Type: application/json',
        // Derived from your own order id, so a retry cannot record it twice.
        'Idempotency-Key: order-1042',
    ],
]);

$revenue = json_decode(curl_exec($ch), true);
echo $revenue['id'];
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "ab_...");

using var request = new HttpRequestMessage(
    HttpMethod.Post, "https://argorobots.com/v1/revenue")
{
    Content = JsonContent.Create(new
    {
        description = "Order #1042",
        amount = 11300,
        currency = "usd",
        tax_amount = 1300,
        occurred_on = "2026-08-14",
        reference = "1042",
    }),
};

// Derived from your own order id, so a retry cannot record it twice.
request.Headers.Add("Idempotency-Key", "order-1042");

using var response = await http.SendAsync(request);
var revenue = await response.Content.ReadFromJsonAsync<JsonElement>();

Console.WriteLine(revenue.GetProperty("id").GetString());
const res = await fetch("https://argorobots.com/v1/revenue", {
  method: "POST",
  headers: {
    Authorization: "Bearer ab_...",
    "Content-Type": "application/json",
    // Derived from your own order id, so a retry cannot record it twice.
    "Idempotency-Key": "order-1042",
  },
  body: JSON.stringify({
    description: "Order #1042",
    amount: 11300,
    currency: "usd",
    tax_amount: 1300,
    occurred_on: "2026-08-14",
    reference: "1042",
  }),
});

const revenue = await res.json();
console.log(revenue.id);
import requests

revenue = requests.post(
    "https://argorobots.com/v1/revenue",
    headers={
        "Authorization": "Bearer ab_...",
        # Derived from your own order id, so a retry cannot record it twice.
        "Idempotency-Key": "order-1042",
    },
    json={
        "description": "Order #1042",
        "amount": 11300,
        "currency": "usd",
        "tax_amount": 1300,
        "occurred_on": "2026-08-14",
        "reference": "1042",
    },
).json()

print(revenue["id"])

Conventions

Money is an integer

Amounts are in the currency's smallest unit, as on Stripe. 1999 means 19.99 USD. Zero-decimal currencies such as JPY have no minor unit, so 1000 means 1000 JPY.

A decimal is rejected, not rounded. Sending 19.99 returns 400 parameter_invalid_amount. Silently rounding somebody's accounting data is not a favour.

Dates and times

Dates you send are YYYY-MM-DD. Timestamps we return are unix integers, so you never have to guess our timezone.

Pagination

Cursor-based. limit is 1 to 100 and defaults to 10. Pass starting_after=<id> for the next page or ending_before=<id> for the previous one. Lists are newest first, and every list has has_more.

There is no offset parameter, deliberately. The merchant's copy of Argo Books drains this queue while you write to it, and an offset would silently skip rows.

Expansion

Reference fields hold an id by default. Pass expand[]=customer to get the whole object instead. Expenses and revenue also accept expand[]=line_items. One level only.

Idempotency

Every create requires an Idempotency-Key header. Retry with the same key and you get the original response back with Idempotent-Replayed: true, for 24 hours. Reuse a key with a different body and you get 409 idempotency_key_reused, because that is a bug on your side rather than a retry.

Versioning

Send Argo-Version: 2026-08-18 to pin. Omit it to track the current version. An unrecognised value is a 400 rather than a silent fallback to something you did not ask for.

Rate limits

120 requests per minute per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Over the limit returns 429 with Retry-After.

Server-side only

The API answers no CORS preflight, and OPTIONS returns 405. A secret key must never be in a browser, and refusing cross-origin requests is the cheapest way to stop that happening by accident.

Request ids

Every response carries a Request-Id header, and every error repeats it in the body. Quote it in a support email and we can find the exact request.

Esc