Skip to content

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

MethodURL
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

  1. Call any of the voucher endpoints (e.g. GET /tally/journal) and receive a list of vouchers, each with an _internalMetadata field.
  2. Import the vouchers into Tally.
  3. For every voucher that imported successfully, save its _internalMetadata string verbatim.
  4. Call POST /tally/record with the strings grouped by voucher type.
  5. The server processes each entry independently and returns a per-item result so you know exactly which records were marked and which failed.
  6. 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>"]
}
FieldTypeRequiredDescription
journalsstring[]NoMetadata blobs from GET /tally/journal responses
salesstring[]NoMetadata blobs from GET /tally/sales responses
receiptsstring[]NoMetadata blobs from GET /tally/receipt responses
purchasestring[]NoMetadata blobs from GET /tally/purchase responses
paymentstring[]NoMetadata 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 still 200. Failures are surfaced per item, not as a top-level error.

Result object

FieldTypeDescription
metadatastringThe exact base64 blob you sent, echoed back so you can correlate results with your input
successbooleantrue if the record was found and marked, false otherwise
errorstringOnly present when success is false. A short reason. See the table below

Possible error reasons

ErrorMeaning
Invalid or unparseable metadataThe 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 sourceIdThe metadata is corrupt or was modified
Invalid supplierIDThe metadata is corrupt or was modified
Invalid paymentIndexThe metadata is corrupt or was modified
Source document not foundThe 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.