Appearance
Recording Vouchers
The Recording endpoint is how you tell the system "I've imported these vouchers into Tally, stop returning them on the next fetch." It is the second half of every integration loop.
You call it with the opaque _internalMetadata blobs you collected from the voucher endpoints, grouped by voucher type. The next time you call a voucher endpoint, anything you successfully recorded is filtered out.
Endpoint
| Method | URL |
|---|---|
POST | /tally/record |
Content-Type: application/json. The body is an object with up to five buckets, each containing an array of base64 metadata strings.
How it works
- Call any of the voucher endpoints (e.g.
GET /tally/journal) and receive a list of vouchers, each with an_internalMetadatafield. - Import the vouchers into Tally.
- For every voucher that imported successfully, save its
_internalMetadatastring verbatim. - Call
POST /tally/recordwith the strings grouped by voucher type. - The server processes each entry independently and returns a per-item result so you know exactly which records were marked and which failed.
- The next time you call the corresponding voucher endpoint, anything you successfully recorded is filtered out.
Request body
json
{
"journals": ["<base64>", "<base64>"],
"sales": ["<base64>"],
"receipts": ["<base64>"],
"purchase": ["<base64>"],
"payment": ["<base64>"]
}| Field | Type | Required | Description |
|---|---|---|---|
journals | string[] | No | Metadata blobs from GET /tally/journal responses |
sales | string[] | No | Metadata blobs from GET /tally/sales responses |
receipts | string[] | No | Metadata blobs from GET /tally/receipt responses |
purchase | string[] | No | Metadata blobs from GET /tally/purchase responses |
payment | string[] | No | Metadata blobs from GET /tally/payment responses |
Bucket names are not all plural
purchase and payment are singular. journals, sales, and receipts are plural. Sending purchase_vouchers, payment_vouchers, purchases, or payments will be silently ignored.
At least one bucket must be non-empty
Sending {} or all-empty arrays returns:
json
{
"status": "error",
"data": "No vouchers provided to record",
"message": "No vouchers provided to record"
}with HTTP 400.
Response
The response envelope is { status, data, message } (see Overview). On success (HTTP 200), data is an object with the same five buckets as the request body. Each bucket is an array of result objects, one per metadata blob you sent, in the same order.
json
{
"status": "success",
"data": {
"journals": [{ "metadata": "...", "success": true }],
"sales": [],
"receipts": [{ "metadata": "...", "success": true }],
"purchase": [{ "metadata": "...", "success": false, "error": "Source document not found" }],
"payment": []
},
"message": "Recording completed with 1 failure(s)"
}The message field summarizes the outcome:
"All vouchers recorded successfully"when every entry succeeded."Recording completed with N failure(s)"when some entries failed. HTTP is still200. Failures are surfaced per item, not as a top-level error.
Result object
| Field | Type | Description |
|---|---|---|
metadata | string | The exact base64 blob you sent, echoed back so you can correlate results with your input |
success | boolean | true if the record was found and marked, false otherwise |
error | string | Only present when success is false. A short reason. See the table below |
Possible error reasons
| Error | Meaning |
|---|---|
Invalid or unparseable metadata | The string was not valid base64, or the decoded content was not a JSON object |
Metadata type mismatch (expected X, got Y) | A blob was placed in the wrong bucket |
Invalid sourceId | The metadata is corrupt or was modified |
Invalid supplierID | The metadata is corrupt or was modified |
Invalid paymentIndex | The metadata is corrupt or was modified |
Source document not found | The underlying record was deleted between fetch and record |
Unknown source: <s> | The metadata is corrupt or was modified |
If you see any of the corrupt-metadata errors with unmodified blobs from a recent fetch, refetch the voucher and try again.
Example request
bash
curl -X POST https://api.example.com/tally/record \
-H "x-api-key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"journals": [
"eyJ0Ijoiam91cm5hbCIsImlkIjoiNjVmMWEyYjNjNGQ1ZTZmN2E4YjljMGQxIn0=",
"eyJ0Ijoiam91cm5hbCIsImlkIjoiNjVmMWEyYjNjNGQ1ZTZmN2E4YjljMGQyIn0="
],
"sales": [
"eyJ0Ijoic2FsZXMiLCJpZCI6IjY1ZjFhMmIzYzRkNWU2ZjdhOGI5YzBkMyJ9"
],
"receipts": [
"eyJ0IjoicmVjZWlwdCIsImlkIjoiNjVmMWEyYjNjNGQ1ZTZmN2E4YjljMGQzIn0="
],
"purchase": [
"eyJ0IjoicHVyY2hhc2UiLCJzIjoicHIiLCJpZCI6IjY1ZjFhMmIzYzRkNWU2ZjdhOGI5YzBlMSJ9"
],
"payment": [
"eyJ0IjoicGF5bWVudCIsInMiOiJwciIsImlkIjoiNjVmMWEyYjNjNGQ1ZTZmN2E4YjljMGUxIiwicGkiOjB9"
]
}'js
const recordRes = await fetch("https://api.example.com/tally/record", {
method: "POST",
headers: {
"x-api-key": process.env.TALLY_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ journals: importedMetadata }),
});
const { data: results } = await recordRes.json();
const failed = results.journals.filter((r) => !r.success);
if (failed.length > 0) {
console.error("Some recordings failed:", failed);
}Example response (all successful)
json
{
"status": "success",
"data": {
"journals": [
{
"metadata": "eyJ0Ijoiam91cm5hbCIsImlkIjoiNjVmMWEyYjNjNGQ1ZTZmN2E4YjljMGQxIn0=",
"success": true
}
],
"sales": [
{
"metadata": "eyJ0Ijoic2FsZXMiLCJpZCI6IjY1ZjFhMmIzYzRkNWU2ZjdhOGI5YzBkMyJ9",
"success": true
}
],
"receipts": [],
"purchase": [
{
"metadata": "eyJ0IjoicHVyY2hhc2UiLCJzIjoicHIiLCJpZCI6IjY1ZjFhMmIzYzRkNWU2ZjdhOGI5YzBlMSJ9",
"success": true
}
],
"payment": [
{
"metadata": "eyJ0IjoicGF5bWVudCIsInMiOiJwciIsImlkIjoiNjVmMWEyYjNjNGQ1ZTZmN2E4YjljMGUxIiwicGkiOjB9",
"success": true
}
]
},
"message": "All vouchers recorded successfully"
}Example response (mixed success and failure)
json
{
"status": "success",
"data": {
"journals": [
{
"metadata": "eyJ0Ijoiam91cm5hbCIsImlkIjoiNjVmMWEyYjNjNGQ1ZTZmN2E4YjljMGQxIn0=",
"success": true
},
{
"metadata": "this-is-not-base64",
"success": false,
"error": "Invalid or unparseable metadata"
}
],
"sales": [],
"receipts": [],
"purchase": [
{
"metadata": "eyJ0IjoicHVyY2hhc2UiLCJzIjoicHIiLCJpZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9",
"success": false,
"error": "Source document not found"
}
],
"payment": []
},
"message": "Recording completed with 2 failure(s)"
}The HTTP status is still 200. Per-item failures do not fail the whole request. Always inspect each result's success flag.
Idempotency
Re-posting is safe
Re-posting the same metadata blobs returns success: true again on every call. There is no harm in retrying after a network error or replaying a batch you are not sure landed.
success: true does not prove this was the first time the voucher was recorded. It just proves the source record exists and is now marked. If you need to detect duplicates on your end, keep your own log of which metadata blobs you have successfully posted.
Common mistakes
Sending raw voucher IDs instead of the metadata blob
The buckets expect the base64 _internalMetadata strings that came back from the GET responses, not the voucherid values.
Using the wrong bucket name
Use purchase (singular) and payment (singular). Anything else is silently ignored. The result is a 400 because the server thinks you sent an empty payload.
Inspecting or constructing the metadata blob
The blob is opaque on purpose. Do not parse it, do not store its decoded contents, do not try to construct one. Future server changes may alter its internal shape.
Treating the response as all-or-nothing
Every entry is processed independently. A 200 response can still contain failures. Always loop over each bucket's results and check success per item.