Taxually Public API

Documentation

Taxually Public API

The Taxually Public API lets partners run tax compliance end to end without a person in a browser: upload transaction data for a month, have Taxually process it into tax returns, approve those returns, and download the finished filing artifacts. Authentication is by API key or JWT, all requests and responses are JSON, and progress is tracked by polling.

Version v1 · docs updated 2026-10-14 · AI agent? Start at For AI agents — a machine-readable endpoint catalogue is embedded in this page as <script id="api-index">.

Overview

What this API is

The Taxually Public API is a partner-facing REST API for automating tax compliance. It covers the working part of the compliance cycle: getting a month's transaction data to Taxually, turning it into tax returns, approving them, and collecting the documents that come out — the return file itself, the readable draft, the analytics workbook, and the proof of submission.

It is built for partners who run compliance for their own clients — marketplaces, accounting platforms, ERP integrators — and want the monthly cycle to run from their own systems instead of through a UI. One integration handles every company your credentials can act on.

The API is organized around REST. It has resource-oriented URLs, accepts JSON request bodies (file uploads are multipart/form-data), returns JSON responses, and uses standard HTTP verbs and status codes. Errors are RFC 7807 problem documents — see Errors & conventions.

The API also publishes a browsable reference of itself at /swagger-public/index.html — https://taxually-test-api.test.taxually.com/swagger-public/index.html on test, https://taxually-api.taxually.com/swagger-public/index.html in production. It needs no credentials to read.

Base URLs

EnvironmentBase URL
Testhttps://taxually-test-api.test.taxually.com
Productionhttps://taxually-api.taxually.com

All endpoints live under the prefix /api/public/v1. The API is available on both environments. The samples throughout these docs use $BASE for the base URL.

Before you call anything

Two things happen once, in the Taxually UI (or your white-label version of it), before the API becomes useful:

  1. The company is set up. Registration and onboarding — the legal entity, its VAT registrations, its subscriptions — happen in the UI. There is no API endpoint for creating a company today.
  2. You create an API key on the company settings page in the Taxually UI. If you cannot see the settings page, contact your Taxually representative.

After that, everything in the monthly cycle — from uploading data to downloading the finished return — runs through the API alone. See Authentication for how to send the key.

The whole process at a glance

This is the full path from raw transaction data to a filed VAT return. Every other page in these docs is a closer look at one part of it. To run through it hands-on, start with Quickstart: generate your first tax return.

  1. Find your companies. GET /companies lists the companies your credentials can act on. Each company's id is the companyId every other endpoint takes. → Companies

  2. Teach the engine your file format (optional). "The engine", throughout these docs, is Taxually's processing engine — the system that reads your files and calculates the returns. If your transaction files come from a common system — Amazon, Shopify, SAP, Oracle and many others — Taxually already knows how to read them, and you skip this step. For your own custom format, create a file setup: GET /mapping-properties lists the transaction properties a column can be mapped onto, then POST /companies/{companyId}/file-setups describes your file — date format, separators, and which column carries which property. This is one-time work per format, not per month. → File setups & mapping

  3. Find your upload window. GET /companies/{companyId}/data-periods lists the months currently open for upload. Which months are open follows from the company's VAT number validity (the date range its VAT registration is valid for) and its subscriptions. → Uploading data & processing

  4. Upload the month's transaction files. POST /companies/{companyId}/data-periods/{period}/transaction-files, one call per file. Uploading only stores the file — nothing starts yet, so you can upload several files before moving on.

  5. Start processing. POST /companies/{companyId}/data-periods/{period}/processing kicks off one run that goes all the way through to return generation — there is no separate "generate returns" step. The call returns immediately; the work continues in the background.

  6. Poll until processing finishes. GET /companies/{companyId}/data-periods/{period} reports the period's state (new, processing, completed, error) plus per-file statistics: transactions read, errors and warnings found. Polling is the model today; webhooks are coming soon.

  7. Check the returns that came out. GET /companies/{companyId}/return-periods lists the returns processing produced — one month of data can yield several (a German VAT return, a Polish VAT return, a German ESL). Each carries its status, VAT amount, currency, and deadlines. GET /companies/{companyId}/return-periods/{returnPeriodId} follows one return. → Returns, approval & submission

  8. Review and approve. Download the return's draft and analytics workbook to review it (step 10 covers how), then POST /companies/{companyId}/return-periods/{returnPeriodId}/approvals records your sign-off — the signal to Taxually that the return should be submitted. Companies can be configured to require two approvals from two different users on returns that have a VAT amount.

  9. Record the filing yourself (only if you submit). When your side files with the tax authority instead of Taxually, POST /companies/{companyId}/return-periods/{returnPeriodId}/submission-proof uploads the authority's proof document and marks the return as submitted. When Taxually files, skip this — Taxually stores the proof.

  10. Download the artifacts. GET /companies/{companyId}/return-periods/{returnPeriodId}/documents lists everything the return produced; GET .../documents/{documentId}/download returns a short-lived download link for each. Per uploaded file, GET .../transaction-files/{fileId}/error-catalogue fetches the spreadsheet of errors and warnings processing found in it. → Documents & error catalogue

Steps 3–10 repeat every month. Steps 1 and 2 you do once.

Key concepts

Company

The legal entity all data belongs to. Set up once via the Taxually UI; from then on it is the companyId in every URL. GET /companies lists exactly the companies your credentials can act on — there is no parameter to widen that set, and a company you cannot access is reported as 404, identically to one that does not exist.

Data period

A month, written yyyy-MM (for example 2026-06), that holds a company's transaction data: its uploaded files and a processing state. One data period per company per month. A period with status new is the one ready to receive data; which months exist is driven by the company's VAT number validity and its subscriptions.

Return period

A specific tax return within a period: one jurisdiction, one return type, one period — for example, the German VAT return for June 2026. One data period can produce several return periods. A return period moves through a lifecycle: new → in-progress → waiting-for-approval → approved → submitted (or error).

File setup

The description of one shape of transaction file: how the engine reads it (date format, separators, filename pattern) and which column carries which transaction property. A file that matches no setup cannot be processed. Global mappings for common systems exist out of the box; you only create a setup for your own formats.

Mapping property

A transaction property a file column can be mapped onto, such as TransactionDate, each with the kind of value it expects (date, decimal, integer, boolean, text). The valid names come from GET /mapping-properties and are case-sensitive.

Error catalogue

A spreadsheet produced per transaction file during processing, listing the errors and warnings found in that file. It only exists if processing found something — a file that processed cleanly has none, reported as 404.

Document

Anything a return period produced: the submission file (the return in the tax authority's format), the human-readable draft, the analytics workbook, any custom extra file, and — once filed — the proof of submission. Documents are downloaded via short-lived links: fetch the link, follow it straight away, never store the URL.

Approval

Your sign-off that a return should be submitted. One call approves. Companies can be configured to require two approvals from two different users on returns that have a VAT amount.

Submission proof

The evidence the tax authority issued when the return was filed — a PDF or an image. Uploading it marks the return period as submitted. Only relevant when you file the return yourself; when Taxually submits, Taxually stores the proof.

How you track progress

Everything long-running in this API follows the same pattern: a POST starts the work and answers immediately with a Location header pointing at the resource to poll; you GET that resource until its status settles. Starting processing points you at the data period; approving a return points you at the return period.

Webhooks — being notified when a period finishes processing or a return is generated, instead of polling — are coming soon. They are not available today; do not build against them yet.

What the API does not cover today

  • Creating companies and onboarding — done in the Taxually UI (see Before you call anything above).
  • Webhooks — coming soon, as above.
  • Fine-tuning beyond column mappings (tax code mappings, categorization rules, adjustments) — coming soon.

Where to go next

Quickstart: generate your first tax return

Go from an empty terminal to a downloaded, submission-ready VAT return. Ten steps, all curl. Roughly 15 minutes of your time — plus however long processing takes in the background.

This walkthrough follows one company through one month: Bergmann Home & Living GmbH, a German home-goods seller trading in Germany, Poland and France, generating its returns for June 2026. Every request and response below uses that company, so you can trace the same IDs from the first call to the last download.

The flow you are about to run is the whole product in miniature:

  1. Check your companies — find the companyId everything else needs
  2. (Optional) Teach the engine to read your files — only for custom file formats
  3. Find the open data period — which month is accepting data
  4. Upload your transaction files — one call per file
  5. Start processing — one call kicks off everything through return generation
  6. Poll until processing completes — watch the status, read the per-file statistics
  7. Inspect the returns — the returns the month produced, with amounts and deadlines
  8. Approve a return — your sign-off that it should be submitted
  9. List the return's documents — the return file, draft, and analytics
  10. Download — fetch the actual files via short-lived links

Before you start

You need two things, and both come from the Taxually UI — company setup is not an API operation today:

  • A company set up in Taxually. Registration and onboarding happen once, through the Taxually UI (or your white-label version of it). After that, everything in this guide runs through the API.
  • An API key. Created on the company settings page in the Taxually UI. If you don't see the option there, contact your Taxually representative.

This quickstart runs against test, and access to test is provisioned by Taxually — if you do not have a test company and a test-UI login yet, ask your Taxually representative for both before starting.

The API also accepts a JWT via Authorization: Bearer <jwt> as an alternative to the API key. This guide uses the API key throughout; see Authentication for both schemes.

Set up your shell so you can paste the samples below straight into a terminal:

export TAXUALLY_API_KEY="<your-api-key>"
export BASE="https://taxually-test-api.test.taxually.com"   # test
# Production: https://taxually-api.taxually.com

The API is available on both environments.

Every endpoint lives under /api/public/v1. Every request is authenticated with the Authorization: Bearer header. Every response is JSON; every error is an RFC-7807 problem document (see Errors & conventions).

Step 1: Check your companies

Everything in this API belongs to a company, and every endpoint after this one is addressed by a companyId. So the first call answers the first question: which companies can these credentials act on?

The answer follows entirely from your credentials — there is no parameter to widen it. The companies this endpoint lists are exactly the companies every other endpoint will accept.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies?limit=50&offset=0"
[
  {
    "id": 48213,
    "name": "Bergmann Home & Living GmbH",
    "countryCode": "DE"
  },
  {
    "id": 51877,
    "name": "Nordwind Outdoor GmbH",
    "countryCode": "DE"
  }
]

What you need from this: the id of the company you're working with — here 48213. That value is the {companyId} in every URL for the rest of this guide.

The list is paged: limit between 1 and 100 (default 50), offset from 0. Keep advancing offset by limit until fewer than limit companies come back. Order is stable, so nothing is skipped or repeated.

One thing worth knowing before you hit a confusing 404 later: a company your credentials cannot access is reported as 404, exactly like a company that doesn't exist. The API deliberately never reveals the existence of companies you can't act on. If a call in this guide 404s unexpectedly, check the companyId against this list first.

Step 2 (optional): Teach the engine to read your files

Skip this step if your transaction data comes from a system Taxually already understands. Global mappings for common commerce and enterprise systems — Amazon, Shopify, Stripe, WooCommerce, SAP, Oracle and more — exist out of the box. Bergmann's amazon_de_2026-06.csv needs nothing: it's recognized automatically.

You need this step when you upload a file in a format that's yours alone — an export from your own webshop or ERP. A file setup teaches the processing engine two things: how to read the file (date format, separators, an optional filename pattern) and which column carries which transaction property. An uploaded file that matches no setup — global or custom — cannot be processed.

First, get the list of valid property names. These are the values you'll send as propertyName, spelled exactly as listed — they are case-sensitive, and a name not on the list is rejected:

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/mapping-properties"
[
  { "name": "TransactionDate", "type": "date" }
]

The sample above is truncated to a single property for illustration — call the endpoint against the test environment for the complete, current list; it is the authoritative source of propertyName values.

The type tells you what the mapped column has to contain: date, decimal, integer, boolean or text.

Now create the setup. Bergmann's webshop exports a CSV whose date column is headed invoice_date, so we map that column onto TransactionDate, flag it as required for VAT detection (that flag is how an uploaded file gets recognized as belonging to this setup), and set a filename pattern so no other setup competes for these files:

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Webshop CSV",
    "dateFormat": "yyyy-MM-dd",
    "decimalSeparator": ".",
    "thousandSeparator": ",",
    "fileNamePattern": "^webshop_orders_.*\\.csv$",
    "columnMappings": [
      {
        "fieldName": "invoice_date",
        "propertyName": "TransactionDate",
        "required": true,
        "requiredForVatDetection": true,
        "requiredForIntrastatDetection": false,
        "requiredForGlDetection": false
      }
    ]
  }' \
  "$BASE/api/public/v1/companies/48213/file-setups"

You get 201 Created and the setup back exactly as stored:

{
  "name": "Webshop CSV",
  "dateFormat": "yyyy-MM-dd",
  "decimalSeparator": ".",
  "thousandSeparator": ",",
  "fileNamePattern": "^webshop_orders_.*\\.csv$",
  "columnMappings": [
    {
      "fieldName": "invoice_date",
      "propertyName": "TransactionDate",
      "required": true,
      "requiredForVatDetection": true,
      "requiredForIntrastatDetection": false,
      "requiredForGlDetection": false
    }
  ]
}

What you need from this: nothing to carry forward — the setup applies automatically to matching files you upload from now on. If the request is rejected (400), the response carries an errors array listing every problem at once, each pointing at the column mapping it concerns, so you fix everything in one pass.

Send the setup as one complete call rather than building it up over several — each write versions the whole mapping configuration, and a file uploaded between two half-finished writes is read under whichever version happens to be current. To change a setup later, replace it wholesale with PUT; there is no patch. Details in File setups & mapping.

Step 3: Find the open data period

Data flows into Taxually month by month. A data period is one month (yyyy-MM) that is open for uploading transaction data; which months are open is driven by the company's VAT number validity and its subscriptions, not by anything you create. So before uploading, ask which periods exist and what state they're in:

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods"
[
  { "period": "2026-07", "status": "new" },
  { "period": "2026-06", "status": "new" },
  { "period": "2026-05", "status": "completed" }
]

Newest first. Each period carries one of four statuses:

StatusMeaning
newReady to receive data; nothing processed yet.
processingBeing worked on — keep polling.
completedCalculated; its returns are available.
errorProcessing failed and you have to act.

What you need from this: the period string of the month you're filing for — here 2026-06, which is new and ready for data. It's the {period} path segment in the next three steps.

Step 4: Upload your transaction files

Now get June's data to Taxually. One call per file, multipart/form-data, exactly one file per call under the form field file. Uploading does not start anything — files just accumulate in the period until you explicitly start processing, so you can upload from several sources at your own pace.

Bergmann has two files for June: the Amazon settlement report and the webshop export from Step 2.

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -F "file=@amazon_de_2026-06.csv" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files"

You get 201 Created — a receipt, not a result. The file is stored, not yet processed, so there are no statistics yet:

{
  "id": "8c2f4a6e-1b3d-45f7-9a8c-2e4d6f8a0b1c",
  "name": "amazon_de_2026-06.csv"
}

Repeat for the second file:

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -F "file=@webshop_orders_2026-06.csv" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files"
{
  "id": "3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c",
  "name": "webshop_orders_2026-06.csv"
}

What you need from this: each file's id. You'll see the same IDs again in the processing statistics (Step 6), and they're what the error-catalogue endpoint takes if a file turns out to have problems.

Two conflicts to know about, both 409: you can't upload while the period is already processing, and you can't upload a file under a name that already exists in the period — the error tells you to delete the old one or rename the new one.

Step 5: Start processing

All files in? One call starts processing — and processing here means the whole pipeline: reading the files, validating the transactions, calculating the tax positions, and generating the returns. There is no separate "finalize" or "generate" step to call afterwards.

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/processing"
HTTP/1.1 202 Accepted
Location: /api/public/v1/companies/48213/data-periods/2026-06

No response body — 202 Accepted means the work continues in the background after the response returns.

What you need from this: the Location header. It points at the exact resource to poll for progress, which is the next step.

Three things earn a 409 here: the period is already processing, the period has no files to process, or a file in the period previously failed to process (delete it and upload a corrected file first). Each detail message says exactly which.

Step 6: Poll until processing completes

Processing takes time — how much depends on data volume. Today the model is polling: request the data period status until it settles. (Webhooks for events like "data period processed" are coming soon; they are not available yet.)

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06"

While it runs you'll see "status": "processing" — keep polling. When it settles:

{
  "period": "2026-06",
  "status": "completed",
  "files": [
    {
      "id": "8c2f4a6e-1b3d-45f7-9a8c-2e4d6f8a0b1c",
      "name": "amazon_de_2026-06.csv",
      "transactionCount": 18452,
      "errorCount": 0,
      "warningCount": 37
    },
    {
      "id": "3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c",
      "name": "webshop_orders_2026-06.csv",
      "transactionCount": 2311,
      "errorCount": 3,
      "warningCount": 12
    }
  ]
}

status: completed means the month has been calculated and its returns exist. The files array tells you, per uploaded file, how many transactions were read and how many errors and warnings processing found in them.

What you need from this: confirmation of completed, and a look at the per-file counts. Non-zero errorCount or warningCount? Fetch that file's error catalogue — a spreadsheet listing every issue found in that file:

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files/3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c/error-catalogue"
{
  "name": "webshop_orders_2026-06_error_catalogue.xlsx",
  "url": "https://storage.taxually.com/documents/…?sig=<short-lived-signed-token>"
}

The link expires shortly after it is issued — download immediately, never store the URL. A file that processed cleanly has no catalogue, which the API reports as 404; that 404 is good news.

If instead the period settles on "status": "error", the response carries an error field with a short human-readable reason, and the per-file counts point at the file that failed. Fix the data and see Uploading data & processing for the recovery path.

Step 7: Inspect the returns

Processing produced returns — now look at them. A return period is one specific tax return: one jurisdiction, one return type, one period. One month of data typically produces several: Bergmann's June yields a German VAT return, a Polish VAT return, and a German EC Sales List.

List them, filtered to June:

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods?dateFrom=2026-06-01&dateTo=2026-06-30"
[
  {
    "id": 90417,
    "country": "DE",
    "returnType": "vat-return",
    "periodStart": "2026-06-01",
    "periodEnd": "2026-06-30",
    "status": "waiting-for-approval",
    "amount": 18432.55,
    "currency": "EUR",
    "isNil": false,
    "submissionDeadline": "2026-08-10",
    "paymentDeadline": "2026-08-10"
  },
  {
    "id": 90418,
    "country": "PL",
    "returnType": "vat-return",
    "periodStart": "2026-06-01",
    "periodEnd": "2026-06-30",
    "status": "waiting-for-approval",
    "amount": 9214.30,
    "currency": "PLN",
    "isNil": false,
    "submissionDeadline": "2026-07-27",
    "paymentDeadline": "2026-07-27"
  },
  {
    "id": 90419,
    "country": "DE",
    "returnType": "esl",
    "periodStart": "2026-06-01",
    "periodEnd": "2026-06-30",
    "status": "waiting-for-approval",
    "amount": null,
    "currency": "EUR",
    "isNil": false,
    "submissionDeadline": "2026-07-27",
    "paymentDeadline": null
  }
]

Reading this: Bergmann owes EUR 18,432.55 in German VAT and PLN 9,214.30 in Polish VAT for June, both due by their paymentDeadline. The ESL is a listing, so it carries no amount — amount: null there is normal, not an error. All three are waiting-for-approval: calculated, and waiting for your sign-off.

The date filters (dateFrom, dateTo) match against the start of the period the return covers, not against deadlines. You can also filter by status. The list is not paged — the whole history comes back.

What you need from this: the id of each return you want to act on. We'll take the German VAT return, 90417, through the rest of the flow. To watch a single return instead of the whole list, fetch it directly — this is also your polling target while a return is still in-progress:

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417"

Return lifecycle, in order: new → in-progress → waiting-for-approval → approved → submitted (plus error, which Taxually resolves — you can't act on it). Full details in Returns, approval & submission.

Before approving, most teams review the human-readable draft. That's a document of the return period — jump to Step 9 to fetch it first if you want eyes on it; approval doesn't require it.

Step 8: Approve a return

Approval is your sign-off: it tells Taxually this return should be submitted to the tax authority. Only a return in waiting-for-approval can be approved.

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/approvals"

201 Created, with a receipt and a Location header pointing back at the return period:

{
  "approvedAt": "2026-07-14T09:32:11Z"
}

What you need from this: the timestamp, for your records. Fetch the return period again and it now reports "status": "approved" — approved and waiting to be submitted.

One wrinkle worth knowing: a company can be configured to require two approvals from two different users on returns that have a VAT amount. In that case the return keeps reporting waiting-for-approval after your first call, and a repeat call by the same user is refused with 409 — the second approval has to come from someone else.

What happens after approval depends on who files. When Taxually submits (the usual case), you're done — the status moves to submitted when the filing happens, and Taxually stores the proof. If you file with the tax authority yourself, you record the filing afterwards by uploading the proof of submission; see Returns, approval & submission.

Step 9: List the return's documents

Every return period produces artifacts, and this endpoint is the flat list of all of them: the submission file (the return itself, in the tax authority's format), the human-readable draft you'd review before approving, the analytics workbook with the transactions the return was calculated from, any custom extra file, and — once filed — the proof of submission.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/documents"
[
  {
    "id": "f4a6c8e0-2b4d-46f8-a0c2-e4f6a8c0e2b4",
    "name": "DE_VAT_2026-06.xml",
    "type": "submission-file"
  },
  {
    "id": "a1c3e5f7-3b5d-47a9-b2c4-d6e8f0a2c4e6",
    "name": "DE_VAT_2026-06_draft.pdf",
    "type": "draft"
  },
  {
    "id": "b2d4f6a8-4c6e-48b0-83d5-e7f9a1b3d5f7",
    "name": "DE_VAT_2026-06_analytics.xlsx",
    "type": "analytics"
  }
]

What you need from this: the id of the document you want — that's what the download endpoint takes. Note the documents belong to the return file Taxually last produced for the period, so if a return is regenerated, this list changes; list fresh rather than caching document IDs long-term.

Step 10: Download

The final step: turn a document id into an actual file. The endpoint doesn't stream the bytes — it hands you a short-lived download link.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/documents/f4a6c8e0-2b4d-46f8-a0c2-e4f6a8c0e2b4/download"
{
  "name": "DE_VAT_2026-06.xml",
  "url": "https://storage.taxually.com/documents/…?sig=<short-lived-signed-token>"
}

Follow the url immediately:

curl -o DE_VAT_2026-06.xml "<url from the response>"

The link expires shortly after it is issued. Fetch the link and download in one motion, every time; never store the URL. The document has to be one listed on this very return period — an id from another return or company is reported as 404.

That's it. You have Bergmann's German VAT return for June 2026 on disk: uploaded, processed, calculated, approved, and downloaded — entirely through the API.

What you just built

The same ten calls, run monthly, are a complete filing integration:

GET  /companies                                             → companyId
GET  /companies/{companyId}/data-periods                    → open period
POST …/data-periods/{period}/transaction-files              → one per file
POST …/data-periods/{period}/processing                     → 202, starts everything
GET  …/data-periods/{period}                                → poll until completed
GET  /companies/{companyId}/return-periods?dateFrom=…       → returns + amounts
POST …/return-periods/{returnPeriodId}/approvals            → sign-off
GET  …/return-periods/{returnPeriodId}/documents            → document ids
GET  …/documents/{documentId}/download                      → short-lived link

Step 2 (file setups) runs once per file format, not once per month.

Next steps

Authentication

Every request to the Taxually Public API is authenticated. There are no public endpoints — even global reference data such as the mapping properties list requires credentials. This page explains how you get credentials, how to send them, and which environment to send them to. Once you can make one authenticated call, the Quickstart: generate your first tax return walks you through the full flow.

How you get access

Access starts in the Taxually UI, not in the API. The sequence is:

  1. Your company is set up in the Taxually UI. Registration and onboarding — legal entity details, VAT registrations, subscriptions — happen through the Taxually UI (or your white-label version of it). This is a one-time step and it is not an API step today.
  2. You create an API key on the company settings page in the Taxually UI. Once the company exists, open its settings page and create a key there.
  3. You call the API with that key. From this point on, the whole return-generation process — discovering companies, uploading data, processing, approving, downloading documents — runs through the API.

If you cannot see the API key section on the company settings page, contact your Taxually representative.

Getting access to the test environment. Access to test is provisioned by Taxually — ask your Taxually representative for a test company and a login to the test UI before you start building.

Which companies your key can act on is fixed by the credentials themselves. There is no request parameter that widens it. GET /api/public/v1/companies (see Companies) lists exactly the set of companies every other endpoint will accept an identifier for.

The two authentication schemes

The API accepts two schemes. Either one authenticates a request — you do not need both. Both travel in the same Authorization header; what differs is the credential you put after the word Bearer.

SchemeHeaderValue format
API keyAuthorizationBearer <api-key> — the API key value, prefixed with the word Bearer
BearerAuthorizationBearer <jwt> — a JWT, prefixed with the word Bearer

API key is the scheme this documentation uses in every example. Send the key you created on the company settings page in the Authorization header of every request, prefixed with the word Bearer. The key already begins with TX — that prefix is part of the key itself, so paste it exactly as you received it:

curl -H "Authorization: Bearer <your-api-key>" ...

Bearer (JWT) sends a JSON Web Token in the standard Authorization header:

curl -H "Authorization: Bearer <jwt>" ...

How to obtain a JWT for the Bearer scheme is not covered here. If you need Bearer authentication, contact your Taxually representative.

The scope and lifetime of an API key (whether it covers one company or several, and whether it expires) are not documented yet. Neither are three practical details: whether a key created in the test UI works only against the test API, whether you can rotate a key yourself on the settings page, and the revocation path when a key leaks. For all of these, contact your Taxually representative.

Base URLs

EnvironmentBase URL
Testhttps://taxually-test-api.test.taxually.com
Productionhttps://taxually-api.taxually.com

All endpoints live under the prefix /api/public/v1, so a full URL looks like:

https://taxually-test-api.test.taxually.com/api/public/v1/companies

The API is available on both environments.

Each environment also serves the browsable Swagger UI at /swagger-public/index.html: https://taxually-test-api.test.taxually.com/swagger-public/index.html for test, https://taxually-api.taxually.com/swagger-public/index.html for production. Neither needs credentials to read.

The examples across these docs use two shell variables, so you can paste them straight into a terminal:

export BASE="https://taxually-test-api.test.taxually.com"
export TAXUALLY_API_KEY="<your-api-key>"

Your first authenticated call

List the companies your credentials can act on. This is the entry point of the API: the id of each company is the companyId every other endpoint takes.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies?limit=50&offset=0"

You should see a JSON array of companies:

[
  {
    "id": 48213,
    "name": "Bergmann Home & Living GmbH",
    "countryCode": "DE"
  },
  {
    "id": 51877,
    "name": "Nordwind Outdoor GmbH",
    "countryCode": "DE"
  }
]

An empty array ([]) is also a valid answer — it means your credentials authenticate but can act on no companies yet. In that case, check with whoever set up the company that the key was created under the right one.

If the key is missing or wrong, you get a 401 problem document with the title Not authenticated (Content-Type: application/problem+json). The shape looks like this — the detail member, when present, carries the specific reason:

{
  "title": "Not authenticated",
  "status": 401,
  "instance": "/api/public/v1/companies"
}

All error responses follow the same RFC 7807 shape — see Errors & conventions.

Authentication and the 404 rule

One behavior worth knowing before you debug anything: a company your credentials cannot access is reported as 404, identically to a company that does not exist. The API never answers 403 for a foreign company — existence is deliberately not revealed. So if a request against a companyId you believe is correct keeps returning 404 Resource not found, the likely cause is the key, not the identifier: the key was created under a different company than the one you are calling. GET /api/public/v1/companies shows you the truth — everything it lists is reachable, everything else is 404.

Keep your key secret

The API key grants access to your companies' tax data — transaction files, returns, VAT amounts, and the ability to approve returns for submission. Treat it like a password:

  • Never commit it to source control or embed it in client-side code.
  • Pass it via an environment variable or a secrets manager, as the examples on these pages do.
  • If a key leaks, contact your Taxually representative.

Next steps

Companies

Companies are the legal entities all tax data belongs to. Your credentials decide which ones you can see. This page explains where companies come from, how to discover the ones you can act on, and what the company object tells you.

Every other endpoint in the API is addressed by a companyId — data periods, file setups, return periods, documents all live under a company. Before you can upload a single transaction file, you need to know which company you are uploading it for. That is what the two endpoints on this page are for.

There are exactly two:

EndpointWhat it does
GET /api/public/v1/companiesLists the companies your credentials can act on.
GET /api/public/v1/companies/{companyId}Fetches one company by its identifier.

Both are read-only. You cannot create, change, or delete a company through the API.

All samples on this page use the conventions from Authentication: $BASE is the base URL (https://taxually-test-api.test.taxually.com on test; https://taxually-api.taxually.com in production) and $TAXUALLY_API_KEY is your API key.

Where companies come from

Companies are not created through the API. Each company is set up once through the Taxually platform — registration and onboarding happen in the UI, including on white-label deployments. That setup records the company's legal details, VAT registrations, and subscriptions. The API key you authenticate with is also created there, on the company settings page.

Once a company exists, everything else runs through the API: mapping source files (File setups & mapping), uploading transaction data and starting processing (Uploading data & processing), and reviewing, approving, and downloading the returns (Returns, approval & submission).

So in the end-to-end flow of generating a tax return, this page is step zero: confirm which companies you can work with, and pick up the companyId that every later call needs.

How access is decided

Which companies you can see follows entirely from your credentials. There is no parameter to widen the set — GET /companies returns exactly the companies every other endpoint will accept an identifier for. If a company is missing from the list, the fix is on the platform side (or with your Taxually representative), not in the request.

One consequence worth knowing before you write error handling: a company your credentials cannot access is reported as 404, exactly like a company that does not exist. The API never answers 403 for this — it deliberately does not reveal whether a company exists. So when a call under /companies/{companyId}/... returns 404, check both possibilities: the identifier may be wrong, or your credentials may not cover that company.

The company object

Both endpoints return the same object. It is small on purpose — the API gives you what you need to address the company, not its full registration record.

FieldTypeMeaning
idintegerThe identifier of the company. This is the value to send as companyId on every other endpoint.
namestringThe registered legal name of the company.
countryCodestring, nullableTwo-letter code of the country the company is established in. Null when no establishment country is recorded.
{
  "id": 48213,
  "name": "Bergmann Home & Living GmbH",
  "countryCode": "DE"
}

Note what the object does not tell you: it says nothing about which countries the company files returns in. countryCode is where the company is established, not where it is registered for VAT. Bergmann Home & Living GmbH is established in Germany but files Polish returns too — you see that in its return periods (Returns, approval & submission), not here.

List companies

GET /api/public/v1/companies returns the companies your credentials can act on, ordered by name.

Parameters

NameInTypeMeaning
limitqueryintegerOptional. Maximum number of companies to return; between 1 and 100. Defaults to 50.
offsetqueryintegerOptional. Number of companies to skip. Defaults to 0.

Example

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies?limit=50&offset=0"
[
  {
    "id": 48213,
    "name": "Bergmann Home & Living GmbH",
    "countryCode": "DE"
  },
  {
    "id": 51877,
    "name": "Nordwind Outdoor GmbH",
    "countryCode": "DE"
  }
]

Paging through the list

Start at offset=0 and advance by limit until a page comes back with fewer than limit companies — that page is the last one. The ordering by name is stable between calls, so no company is skipped or repeated while the set does not change.

An empty array is a valid answer, not an error. You get one when your credentials can act on no companies at all, or when offset points past the end of the list.

Errors

A limit outside 1–100 or a negative offset is rejected with a 400 problem document. See Errors & conventions for the problem document shape.

Get a company

GET /api/public/v1/companies/{companyId} fetches a single company.

Parameters

NameInTypeMeaning
companyIdpathinteger, requiredThe identifier of the company.

Example

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213"
{
  "id": 48213,
  "name": "Bergmann Home & Living GmbH",
  "countryCode": "DE"
}

Errors

A 404 problem document means the company does not exist — or your credentials cannot access it. The two cases are indistinguishable by design (see How access is decided above).

{
  "title": "Resource not found",
  "status": 404,
  "detail": "No company 48213 exists.",
  "instance": "/api/public/v1/companies/48213"
}

When to list, when to get

List when you are discovering. On first setup of an integration, call GET /companies to see what your key covers, and let a user (or your configuration) pick the company to work with. If you manage many companies — a marketplace or accounting platform acting for its sellers — list is also how you iterate over all of them, for example to check every company's open data periods each month.

Get when you already hold an identifier and want to confirm it. Typical case: a companyId stored in your system's configuration. A quick GET /companies/{companyId} at startup verifies the identifier is still valid and still accessible with the current key — cheaper and more precise than paging through the whole list. It is also the natural health check for a scheduled job before it starts uploading files.

If you only ever act on one company, you can skip listing entirely: fetch it once by identifier, store the id, and move on to the next step of the flow.

Next steps

With a companyId in hand, the flow continues:

File setups & mapping

Before Taxually can turn your transaction files into tax returns, it has to read them. This page explains how the system recognizes the files you upload, when you need to describe a custom format yourself, and how to create and maintain that description — a file setup — through the API.

Most integrations never need this page. If your data comes out of a common commerce or enterprise system — Stripe, Amazon, WooCommerce, Shopify, SAP, Oracle, and many others — Taxually already knows how to read it. Global mappings for these formats exist out of the box: you upload the file (see Uploading data & processing) and the engine recognizes it with no setup on your side.

You need a file setup only when your files are in a format of your own — an export from your webshop, your ERP's custom report, a bespoke CSV your finance team produces. A file setup teaches the engine two things:

  1. How to parse the file — its date format, decimal and thousand separators, and optionally a filename pattern.
  2. Which column carries which transaction property — for example, that your invoice_date column is the transaction date.

An uploaded file that matches no setup — global or custom — cannot be processed. Setting up the mapping is step 2 of the end-to-end flow, before your first upload; see Quickstart: generate your first tax return for the full sequence.

All samples on this page use the demo company Bergmann Home & Living GmbH (companyId 48213), the Authorization: Bearer header (see Authentication), and $BASE as the base URL:

BASE="https://taxually-test-api.test.taxually.com"   # test
# Production: https://taxually-api.taxually.com

How the engine recognizes a file

When you upload a transaction file, the engine looks at its columns and its name to decide which setup reads it:

  • Detection columns. Each column mapping carries flags — requiredForVatDetection, requiredForIntrastatDetection, requiredForGlDetection — that mark the columns a file has to contain to be recognized as carrying that kind of data. At least one column of a setup normally carries the VAT detection flag; a setup with no detection columns will never match any uploaded file.
  • Filename pattern. A setup can also carry a fileNamePattern, a regular expression matched against the uploaded file's name. When set, only matching files are read with that setup.

Two rules of thumb follow:

  • Pick a detection column every file of this shape carries. If your detection column is one some exports omit, those files match nothing, and processing fails because the file's format cannot be determined.
  • Disambiguate overlapping setups with filename patterns. If two setups' detection columns both match a file, the choice between them is arbitrary. Give each a fileNamePattern so it isn't.

Your company may also carry setups Taxually configured for a specific accounting system (SAP, NetSuite, lookup-table based). Those carry settings this API does not expose and cannot be replaced through it — but they are listed alongside yours, because they compete for the same uploaded files.

Global mappings are file setups Taxually maintains centrally for common systems (Amazon, Shopify, Stripe, SAP, Oracle, and others) — they are why most integrations never create a setup at all. They compete for uploaded files the same way your own setups do.

Step 1: Discover the mapping properties

GET /api/public/v1/mapping-properties

Before you write a single column mapping, fetch the list of transaction properties a column can be mapped onto. These names are the only accepted values for propertyName, and they are case-sensitive — transactionDate is rejected, TransactionDate is accepted.

This is global reference data: it takes no company identifier and performs no company access check. Authentication is still required.

Each property also tells you what kind of value the mapped column has to contain:

typeThe column must contain
dateDates
decimalDecimal numbers
integerWhole numbers
booleanTrue/false values
textFree text

How a date or a number is written in your file — 2026-06-30 vs 30.06.2026, 1,234.56 vs 1.234,56 — is a property of your file setup (dateFormat, decimalSeparator, thousandSeparator), not of the property itself.

Request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/mapping-properties"

Response — 200 (array of properties, ordered by name)

[
  { "name": "TransactionDate", "type": "date" }
]

The sample above is truncated to a single property for illustration — the live endpoint returns the complete, current list, and it is the authoritative source of propertyName values.

FieldTypeMeaning
namestringThe name to send as propertyName in a column mapping. Case-sensitive.
typestringWhat the mapped column has to contain: date, decimal, integer, boolean or text.

This list does not tell you which properties your setup must map — that follows from the setup's own detection flags and from what your data has to support. If you are unsure which columns your returns need, contact your Taxually representative.

Step 2: See the active setups

GET /api/public/v1/companies/{companyId}/file-setups

List the file setups the company currently uses to read its transaction files. Only the setups active right now are listed; superseded and deleted ones are not. The list includes any Taxually-configured accounting-system setups — you cannot replace those through the API, but you need to see them to avoid detection-column overlaps.

Request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/file-setups"

Response — 200 (array of setups, ordered by name; empty when the company has none)

[
  {
    "name": "Webshop CSV",
    "dateFormat": "yyyy-MM-dd",
    "decimalSeparator": ".",
    "thousandSeparator": ",",
    "fileNamePattern": "^webshop_orders_.*\\.csv$",
    "columnMappings": [
      {
        "fieldName": "invoice_date",
        "propertyName": "TransactionDate",
        "required": true,
        "requiredForVatDetection": true,
        "requiredForIntrastatDetection": false,
        "requiredForGlDetection": false
      }
    ]
  }
]

Setup fields

FieldTypeMeaning
namestringThe name of the file setup, unique within the company. It is the identifier the setup is addressed by, matched case-insensitively.
dateFormatstring, nullableThe .NET date format the date columns are parsed with, e.g. yyyy-MM-dd. Absent when the setup does not set one.
decimalSeparatorstring, nullableThe single character separating decimals in numeric columns, e.g. .. Absent when not set.
thousandSeparatorstring, nullableThe single character grouping thousands, e.g. ,. Absent when not set.
fileNamePatternstring, nullableA regular expression matched against the name of an uploaded file. When set, only matching files are read with this setup. Absent when the setup is selected by its columns alone.
columnMappingsarrayWhich column of the file carries which transaction property.

Column mapping fields

FieldTypeMeaning
fieldNamestringThe column header as it appears in your file, e.g. invoice_date. Matched case-insensitively, unique within the setup.
propertyNamestringThe transaction property the column is mapped onto, e.g. TransactionDate. One of the names from Step 1.
requiredbooleanWhether a row that leaves this column empty is rejected. Only applies to files that carry the column at all.
requiredForVatDetectionbooleanWhether this column has to be present for the file to be recognized as carrying VAT transactions.
requiredForIntrastatDetectionbooleanWhether this column has to be present for the file to be recognized as carrying Intrastat data.
requiredForGlDetectionbooleanWhether this column has to be present for the file to be recognized as carrying general ledger data.

Errors: 404 when the company does not exist or your credentials cannot access it (see Errors & conventions — inaccessible companies are reported as 404, never 403).

Step 3: Create the file setup

POST /api/public/v1/companies/{companyId}/file-setups

Send the complete setup in one call. Once it is stored, every file you upload whose columns (and name, if you set a pattern) match it is read with it — no further wiring needed.

Send the whole setup at once, not piece by piece. Every write clones the company's whole mapping configuration into a fresh version and marks the mappings as changed, which makes already-processed data periods eligible for reprocessing. A run of small writes leaves a trail of versions behind it, and a file uploaded mid-trail is read under whichever version happens to be current — an intermediate one whose detection column the file does not carry rejects it outright.

Request body (application/json)

FieldTypeMeaning
namestring, requiredThe name of the file setup, unique within the company, at most 30 characters.
dateFormatstring, optional.NET date format for the file's date columns, e.g. yyyy-MM-dd. Use lowercase y and d specifiers; YY and D are not valid. Leave absent to let the engine parse dates without a fixed format.
decimalSeparatorstring, optionalThe single character separating decimals, e.g. ..
thousandSeparatorstring, optionalThe single character grouping thousands, e.g. ,.
fileNamePatternstring, optionalRegex matched against uploaded file names, at most 50 characters. Set it when several setups would otherwise match the same columns.
columnMappingsarray, requiredAt least one mapping; no two mappings may name the same column.

Each mapping carries: fieldName (required, at most 100 characters, unique within the request, matched case-insensitively against the file header), propertyName (required, one of the mapping-property names from Step 1, case-sensitive), and the four booleans required, requiredForVatDetection, requiredForIntrastatDetection, requiredForGlDetection — meanings as in Step 2.

Request

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Webshop CSV",
    "dateFormat": "yyyy-MM-dd",
    "decimalSeparator": ".",
    "thousandSeparator": ",",
    "fileNamePattern": "^webshop_orders_.*\\.csv$",
    "columnMappings": [
      {
        "fieldName": "invoice_date",
        "propertyName": "TransactionDate",
        "required": true,
        "requiredForVatDetection": true,
        "requiredForIntrastatDetection": false,
        "requiredForGlDetection": false
      }
    ]
  }' \
  "$BASE/api/public/v1/companies/48213/file-setups"

Response — 201 — the setup as stored, same shape as a Step 2 list entry. The Location header points at it: /api/public/v1/companies/48213/file-setups/Webshop%20CSV.

Errors

StatusWhen
400The setup was rejected; nothing was stored. The problem document's errors member lists every reason at once — see Validation errors below.
404Company missing or inaccessible.
409The company already has a setup with this name: "A file setup named 'Webshop CSV' already exists for company 48213. Replace it instead of creating it again."

Replacing a file setup

PUT /api/public/v1/companies/{companyId}/file-setups/{setupName}

To change how a custom format is read, replace its setup. This is a wholesale replacement, not a patch: a column mapping your request no longer names is removed from the setup, and a setting you leave out is cleared. The safe pattern is read–modify–write:

  1. GET the setup (Step 2) to see what is stored.
  2. Change what needs changing.
  3. PUT the whole thing back.

Replacing in one call is deliberately the only shape on offer — the same versioning rationale as creating: batch your changes into one call rather than a run of small ones.

The setupName in the URL is matched case-insensitively. In the body, name is optional when replacing; if you send it, it has to match the name in the URL. The stored spelling of the name wins — replacing a setup cannot quietly recase its name. All other body fields are exactly as when creating.

Request

curl -X PUT \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "dateFormat": "yyyy-MM-dd",
    "decimalSeparator": ".",
    "thousandSeparator": ",",
    "fileNamePattern": "^webshop_orders_.*\\.csv$",
    "columnMappings": [
      {
        "fieldName": "invoice_date",
        "propertyName": "TransactionDate",
        "required": true,
        "requiredForVatDetection": true,
        "requiredForIntrastatDetection": false,
        "requiredForGlDetection": false
      }
    ]
  }' \
  "$BASE/api/public/v1/companies/48213/file-setups/Webshop%20CSV"

Response — 200 — the setup as stored, same shape as a Step 2 list entry.

Errors

StatusWhen
400Rejected; the errors member lists every reason. Also returned when the body's name differs from the name in the URL.
404The company has no setup with this name, or the company is inaccessible.
409The setup is one Taxually configured for a specific accounting system and cannot be replaced through this API; the detail tells you to ask Taxually to change it.

There is no delete endpoint for file setups in v1. To remove one, contact your Taxually representative.

Validation errors

When a create or replace request is rejected with 400, nothing is stored, and the problem document carries an errors extension member listing every reason at once — you fix the whole setup in one round trip instead of discovering problems one by one:

{
  "title": "Invalid request",
  "status": 400,
  "detail": "The file setup was rejected and nothing was stored. Every reason is listed in 'errors'.",
  "instance": "/api/public/v1/companies/48213/file-setups",
  "errors": [
    {
      "message": "'transactionDate' is not a transaction property a column can be mapped onto. The mapping properties endpoint lists the accepted names; they are case-sensitive.",
      "columnMappingIndex": 0
    },
    {
      "message": "dateFormat is not a valid .NET date format: 'YY' and 'YYYY' are not year specifiers and 'D' and 'DD' are not day specifiers. Use lowercase 'yy'/'yyyy' for the year and 'd'/'dd' for the day, for example 'yyyy-MM-dd'.",
      "columnMappingIndex": null
    }
  ]
}
FieldMeaning
errors[].messageWhat is wrong, in words you can act on.
errors[].columnMappingIndexThe zero-based position of the offending mapping in the columnMappings array you sent. null when the message concerns the setup as a whole (for example an invalid date format).

Common causes: a propertyName that is not on the mapping-properties list (or is miscased), an invalid .NET date format, two mappings naming the same column, or a name/fieldName/fileNamePattern over its length limit. The general problem-document format is described in Errors & conventions.

What happens next

With the setup stored, the mapping side is done. From here:

  • Upload your files — files matching the setup are read with it during processing. See Uploading data & processing.
  • Check per-file results — after processing, each uploaded file reports transaction, error, and warning counts, and files with findings get an error catalogue. See Uploading data & processing and Documents & error catalogue.
  • Format changed? Your webshop renamed a column, or the export gained a field — replace the setup (above) before uploading files in the new shape.

Full request and response schemas for all four endpoints are in the API reference.

Uploading data & processing

You upload one transaction file per call into a data period — the month that is open for upload. Uploading stores files and nothing more. A single processing call then runs the whole pipeline through to return generation, and you poll the data period to watch it finish.

This page answers two questions: how do you get a month's data to Taxually, and how does that data become returns. It assumes you already have a companyId from the Companies page and, if your files use a custom format, a file setup from File setups & mapping.

All examples run against the demo company Bergmann Home & Living GmbH (companyId 48213) for the period 2026-06, using the API key header described in Authentication:

BASE="https://taxually-test-api.test.taxually.com"   # test
# Production: https://taxually-api.taxually.com

How the pieces fit together

Four calls, in order:

  1. GET /data-periods — find which months are open for upload.
  2. POST /data-periods/{period}/transaction-files — upload the month's files, one call per file.
  3. POST /data-periods/{period}/processing — start processing. One call, no further steps.
  4. GET /data-periods/{period} — poll until the period reports completed or error.

Two things about this flow are deliberate and worth remembering:

  • Uploading starts nothing. Files are stored, not processed. You can upload as many files as the month needs, over hours or days, before anything runs.
  • Processing runs all the way through. The single processing call reads the files, validates the transactions, calculates, and generates the returns. There is no separate "finalize" or "generate returns" call — when the data period reports completed, the returns exist.

When the period completes, you continue on Returns, approval & submission.

Data periods: your upload window

A data period is a month, written yyyy-MM, that holds the company's transaction data. There is one data period per company per month, and it carries two things: the uploaded transaction files and a processing state. The list includes past periods; a period with status new is the one ready to receive data.

You don't create data periods. Which months are open is driven by the company's VAT number validity and its subscriptions — a month only appears once the company is registered and subscribed for it. If a month you expect is missing, that is a company setup question, not an API one: contact your Taxually representative.

List the data periods

GET /api/public/v1/companies/{companyId}/data-periods

Lists the company's data periods, newest first, each with its processing state. Use it to find where this month's data should go.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods"
[
  { "period": "2026-07", "status": "new" },
  { "period": "2026-06", "status": "new" },
  { "period": "2026-05", "status": "completed" }
]
FieldTypeMeaning
periodstringThe data period, in yyyy-MM format.
statusstringProcessing state: new, processing, completed or error. See the status table below.

The list carries only the period and its state. For per-file statistics and failure reasons, request the individual period — that's the polling endpoint, covered below.

Returns 200 with an empty array when the company has no data periods, and a 404 problem when the company doesn't exist or your credentials cannot access it (the two cases are deliberately indistinguishable — see Errors & conventions).

Upload transaction files

POST /api/public/v1/companies/{companyId}/data-periods/{period}/transaction-files

Uploads one transaction file into the data period. One file per call — the body is multipart/form-data with exactly one part, form field file. To upload the month's Amazon export and your webshop export, make two calls.

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -F "file=@amazon_de_2026-06.csv" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files"

A successful upload returns 201 with a receipt. Note that the receipt has no statistics — the file has been stored, not processed:

{
  "id": "8c2f4a6e-1b3d-45f7-9a8c-2e4d6f8a0b1c",
  "name": "amazon_de_2026-06.csv"
}
FieldTypeMeaning
idstring (uuid)The document identifier of the uploaded file. Keep it — the error-catalogue endpoint on Documents & error catalogue takes it.
namestringThe original name of the uploaded file.

The Location header of the 201 points at the data period status resource (/api/public/v1/companies/48213/data-periods/2026-06) — the same URL you will poll after starting processing.

How does the engine know how to read each file? By matching it against the company's active file setups: global mappings cover common systems (Amazon, Shopify, SAP, Oracle, …) out of the box, and custom formats need a file setup you create first. A file that matches no setup cannot be processed. See File setups & mapping.

Accepted file formats and size limits are not published here yet; the examples across these docs use CSV exports. If you are unsure whether your file format is supported, contact your Taxually representative.

Things that make an upload fail:

StatusWhy
400The period isn't yyyy-MM, the request isn't multipart, or the file is empty.
404No data period for that company and month, or the company is inaccessible.
409The period is currently being processed — wait for it to finish.
409A file with the same name already exists in the period. The detail reads: "A file named 'webshop_orders_2026-06.csv' already exists in 2026-06. Delete it or upload under a different name."

Delete a transaction file

DELETE /api/public/v1/companies/{companyId}/data-periods/{period}/transaction-files/{fileId}

Removes one file from the data period — a duplicate, a wrong export, or a file that failed to process — so a corrected file can be uploaded in its place. The fileId is the id from the upload receipt; if you no longer have it, the data period status endpoint lists every file in the period with its id.

curl -X DELETE \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files/8c2f4a6e-1b3d-45f7-9a8c-2e4d6f8a0b1c"

A successful delete returns 204 with no body. The file is retired rather than erased: it stops counting towards the period and disappears from the data period status, but it stays on Taxually's record — and its name is released, so the corrected file can be uploaded under the same name.

Deleting a file does not reprocess anything. Upload the replacement, then start processing again.

Things that make a delete fail:

StatusWhy
400The period isn't yyyy-MM.
404No data period for that company and month, no file with that fileId in it, or the company is inaccessible.
409The period is currently being processed, so its files cannot be changed — wait for it to finish.

Start over: restart the data period

POST /api/public/v1/companies/{companyId}/data-periods/{period}/restart

When the period's data is wrong as a whole — the wrong month's exports, a systematically broken feed — restarting returns the period to the state it was in before any data was uploaded. Every uploaded file is discarded; everything has to be uploaded again before the period can be processed. When only one file is bad, delete that file instead — that leaves the period's other files untouched.

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/restart"

A successful restart returns 200 with the period's state after the restart — the same shape as the data period status endpoint, now with an empty file list.

Things that stop a restart:

StatusWhy
400The period isn't yyyy-MM.
404No data period for that company and month, or the company is inaccessible.
409The period is currently being processed, so it cannot be restarted — wait for it to finish.

Start processing

POST /api/public/v1/companies/{companyId}/data-periods/{period}/processing

Once every file for the month is uploaded, one call starts processing. No request body.

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/processing"
HTTP/1.1 202 Accepted
Location: /api/public/v1/companies/48213/data-periods/2026-06

The 202 has no body. Processing continues in the background after the response; the Location header points at the status resource to poll.

This one call does everything: it reads the uploaded files, validates and enriches the transactions, calculates, and generates the return periods. You never call anything else to make the returns appear — when the data period reports completed, they are on Returns, approval & submission waiting for you.

Things that stop processing from starting:

StatusWhy
400The period isn't yyyy-MM.
404No data period for that company and month, or the company is inaccessible.
409Already processing: "The period is already processing."
409Nothing to process: "There are no files to process in 2026-06. Upload transaction files first."
409A previous file failed: "A file in 2026-06 failed to process. Delete it and upload a corrected file before processing the period."

Poll for the result

GET /api/public/v1/companies/{companyId}/data-periods/{period}

The polling target — the same URL the processing call's Location header gave you. It reports the period's state, the failure reason when it errored, and per-file statistics.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06"

While processing runs, keep polling until the status leaves processing:

StatusMeaningWhat you do
newThe period is ready to receive data; nothing has been processed yet.Upload files, start processing.
processingThe period is being worked on.Keep polling.
completedThe period has been calculated and its returns are available.Move on to Returns, approval & submission.
errorProcessing failed.Read the error field and the per-file statistics; act on them.

When processing finishes cleanly:

{
  "period": "2026-06",
  "status": "completed",
  "files": [
    {
      "id": "8c2f4a6e-1b3d-45f7-9a8c-2e4d6f8a0b1c",
      "name": "amazon_de_2026-06.csv",
      "transactionCount": 18452,
      "errorCount": 0,
      "warningCount": 37
    },
    {
      "id": "3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c",
      "name": "webshop_orders_2026-06.csv",
      "transactionCount": 2311,
      "errorCount": 3,
      "warningCount": 12
    }
  ]
}
FieldTypeMeaning
periodstringThe data period, in yyyy-MM format.
statusstringnew, processing, completed or error.
errorstring, nullableA short, human-readable reason for the failure. Only present when status is error; omitted otherwise.
filesarrayThe transaction files processed for this period. Empty when the period has none.

Each entry in files:

FieldTypeMeaning
idstring (uuid)The document identifier of the uploaded file — the same id the upload receipt returned, and what the error-catalogue endpoint takes.
namestringThe original name of the uploaded file.
transactionCountintegerThe number of transactions read from the file.
errorCountintegerThe number of processing errors found in the file.
warningCountintegerThe number of processing warnings found in the file.

Returns 400 when the period isn't yyyy-MM, and 404 when no data period exists for that company and month or the company is inaccessible.

How often to poll

Poll every 30–60 seconds. Processing time grows with data volume, so a period with hundreds of thousands of transactions takes longer than one with a few thousand — don't treat a long processing state as a failure. If you poll on a tight loop, back off — polling more often will not make processing finish sooner.

Polling is the model today. Webhooks are coming soon — event notifications such as "data period processed" are planned but not available yet, so don't design your integration around them. When they ship, they will replace the polling loop, not the endpoints on this page.

Reading the statistics

The per-file counts tell you how healthy the month's data is, even when the period completed:

  • errorCount > 0 on a completed period means some rows were rejected during processing. The returns were still generated from the transactions that did process, so check whether the rejected rows matter before approving anything.
  • warningCount flags rows the engine processed but wants a human to glance at.
  • For the row-by-row detail behind either count, download the file's error catalogue — a spreadsheet produced per file during processing, fetched by the file's id. A file that processed cleanly has no catalogue. See Documents & error catalogue.

When processing fails

A period in error carries a short reason in the error field:

{
  "period": "2026-06",
  "status": "error",
  "error": "Transaction file processing failed.",
  "files": [
    {
      "id": "3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c",
      "name": "webshop_orders_2026-06.csv",
      "transactionCount": 0,
      "errorCount": 1,
      "warningCount": 0
    }
  ]
}

The files array points at the culprit: a file with transactionCount: 0 and errors was not readable at all — usually a format problem. Check that the file matches a file setup (see File setups & mapping), fix the file or the setup, and note that a period holding a failed file refuses a new processing call with 409 until the failed file is replaced. Delete the failed file, upload the corrected one, and start processing again. If the whole period's data is wrong, restart the period instead and upload everything afresh.

Correcting a period after processing

When one file's data is wrong, delete that file and upload the corrected one in its place; when the period's data is wrong as a whole, restart the period and upload everything again. Either way, request processing again afterwards — neither call reprocesses on its own. The same path corrects a return after approval: restart the period, upload the corrected data, and process again — the period produces fresh returns. Replacing a file setup also marks the company's mappings as changed, which makes already-processed data periods eligible for reprocessing — see File setups & mapping.

What's next

Returns, approval & submission

How do you tell Taxually a return should be submitted? Once processing finishes, Taxually has calculated your returns — but nothing is filed until you say so. This page covers the return periods your data produced, what their statuses mean, how to approve a return, and how to record the filing yourself when you submit to the tax authority instead of Taxually.

This is the step after Uploading data & processing. If you have not generated a return yet, start with the Quickstart: generate your first tax return.

All samples use the API key header and the demo company from the other pages: Bergmann Home & Living GmbH, companyId 48213, data period 2026-06. $BASE is your base URL — https://taxually-test-api.test.taxually.com on the test environment, https://taxually-api.taxually.com in production.

What a return period is

A return period is one specific tax return in a month: one jurisdiction, one return type, one period. The German VAT return for June 2026 is a return period. The Polish VAT return for the same June is a different one.

One data period usually produces several return periods. When Bergmann's June data finishes processing, it produces three: a German VAT return, a Polish VAT return, and a German ESL. Each moves through its own lifecycle independently — you can approve the German return while the Polish one is still being reviewed.

Each return period carries everything you need to decide on it: the VAT amount and currency, whether it is a nil return, and its submission and payment deadlines.

List the returns your data produced

When the data period reports completed (see Uploading data & processing), ask for the return periods it produced.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods?dateFrom=2026-06-01&dateTo=2026-06-30"

GET /api/public/v1/companies/{companyId}/return-periods returns the company's whole history, newest first. The list is not paged. Three optional query filters narrow it down:

ParameterTypeMeaning
dateFromyyyy-MM-ddOnly returns whose period starts on or after this date.
dateToyyyy-MM-ddOnly returns whose period starts on or before this date.
statusstringOnly returns in this state — one of the six statuses below.

Both date filters match against the start of the period the return covers, not against its deadlines. So dateFrom=2026-06-01&dateTo=2026-06-30 means "returns covering June 2026", regardless of when they are due.

[
  {
    "id": 90417,
    "country": "DE",
    "returnType": "vat-return",
    "periodStart": "2026-06-01",
    "periodEnd": "2026-06-30",
    "status": "waiting-for-approval",
    "amount": 18432.55,
    "currency": "EUR",
    "isNil": false,
    "submissionDeadline": "2026-08-10",
    "paymentDeadline": "2026-08-10"
  },
  {
    "id": 90418,
    "country": "PL",
    "returnType": "vat-return",
    "periodStart": "2026-06-01",
    "periodEnd": "2026-06-30",
    "status": "waiting-for-approval",
    "amount": 9214.30,
    "currency": "PLN",
    "isNil": false,
    "submissionDeadline": "2026-07-27",
    "paymentDeadline": "2026-07-27"
  },
  {
    "id": 90419,
    "country": "DE",
    "returnType": "esl",
    "periodStart": "2026-06-01",
    "periodEnd": "2026-06-30",
    "status": "waiting-for-approval",
    "amount": null,
    "currency": "EUR",
    "isNil": false,
    "submissionDeadline": "2026-07-27",
    "paymentDeadline": null
  }
]

Reading the fields

FieldMeaning
idThe return period identifier. Every endpoint on this page takes it.
countryThe jurisdiction the return is filed in.
returnTypeThe kind of return: vat-return, esl, intrastat-arrival, oss, ioss and others — the full list is under Return types on Errors & conventions. An open set: treat unrecognized values (including unknown) as valid.
periodStart / periodEndThe first and last day of the period the return covers.
statusWhere the return is in its lifecycle — see below.
errorWhy producing the return failed. Only present when status is error and the reason is one you can be told.
amountThe VAT amount, in currency. Null while not calculated — and for return types that carry no amount, such as listings. That is why the ESL above shows null: it lists transactions, it does not owe anything.
currencyThe currency the return is reported in. Null when it cannot be resolved.
isNilWhether the return reports nothing for the period. Null while not calculated.
submissionDeadlineThe date the return has to reach the tax authority by. Null when the entry has no submission deadline of its own — a standalone VAT payment (returnType vat-payment), for example, carries only a payment deadline.
paymentDeadlineThe date the amount owed has to be paid by. Null when nothing is payable.

The status lifecycle

A return period reports one of six statuses, in lifecycle order:

StatusMeaningWhat you do
newThe return period exists, but no return has been calculated for it yet.Nothing — upload data and start processing if you haven't.
in-progressThe return is being worked on.Keep polling.
waiting-for-approvalThe return has been calculated and is waiting for your sign-off. Also reported while a required second approval is outstanding.Review it, then approve.
approvedYou have approved it; it is waiting to be submitted.Nothing — unless you file it yourself (see below).
submittedThe return has been submitted to the tax authority.Done. Download the proof from Documents & error catalogue.
errorProducing the return failed.Nothing — Taxually resolves these; there is no action you can take through the API.

These six values are also the only accepted values of the status query filter. Passing anything else gets a 400 problem response whose detail lists all six.

Polling is the model today: check the status until it moves. Webhooks that notify you when a return changes state are coming soon, but not available yet.

Check one return

To follow a single return — the natural polling target between completed processing and waiting-for-approval, and again after you approve — fetch it by id.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417"
{
  "id": 90417,
  "country": "DE",
  "returnType": "vat-return",
  "periodStart": "2026-06-01",
  "periodEnd": "2026-06-30",
  "status": "waiting-for-approval",
  "amount": 18432.55,
  "currency": "EUR",
  "isNil": false,
  "submissionDeadline": "2026-08-10",
  "paymentDeadline": "2026-08-10"
}

A return period that does not exist for this company — including one that belongs to a company your credentials cannot access — is reported as 404.

Before approving, you will usually want to review what was calculated. The return period's draft document is the human-readable version of the return, and the analytics workbook shows the transactions it was calculated from. Both are listed and downloaded as described in Documents & error catalogue.

Approve the return

Approval is your green light: it tells Taxually the return should be submitted. Until you approve, nothing is filed.

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/approvals"

One call approves the return. The response is a receipt, and the Location header points back at the return period so you can watch its status move to approved.

{
  "approvedAt": "2026-07-14T09:32:11Z"
}

201 Created, Location: /api/public/v1/companies/48213/return-periods/90417.

Only a return in waiting-for-approval can be approved. Approving one in any other state gets a 409 problem response explaining why. VAT-group returns are approved in the order the group is set up for.

Companies that require two approvals

A company can be configured to require two approvals on returns that carry a VAT amount. When it is:

  • The first approval is recorded, but the return keeps reporting waiting-for-approval.
  • The second approval must come from a different user. A repeat call by the same user is refused with 409:
{
  "title": "Conflict",
  "status": 409,
  "detail": "This return period was already approved by you; a second approval must come from a different user.",
  "instance": "/api/public/v1/companies/48213/return-periods/90417/approvals"
}
  • Once the second, different user approves, the return moves to approved.

If your integration approves returns with a single credential and the status stays at waiting-for-approval after a successful 201, this is why: the company is waiting for its second approval.

How an API-key call is attributed to a user — and therefore how an integration performs the second of two approvals — is not documented yet. If your company is configured for two approvals, contact your Taxually representative before building the approval step.

What happens after approval

Who submits the return depends on how the engagement is set up:

  • Taxually files it. You are done. Poll the return period until its status reaches submitted, then download the proof of submission from its document list. Do not call the submission-proof endpoint — that is only for filings you made yourself.
  • You file it yourself. Download the submission-file document — the return in the exact format the tax authority accepts — submit it to the authority, and then record the filing as described next.

Approved with wrong data? Restart the data period, upload the corrected files, and process again — the period produces fresh returns to approve.

Record a filing you made yourself

POST …/submission-proof is the alternative ending of the flow, for partners who submit to the tax authority themselves. It records a filing you have already made — it does not ask Taxually to file anything. Skip this endpoint entirely when Taxually does the submission.

Because you are reporting a real-world event, the evidence is required: you upload the proof the tax authority issued — a PDF or an image — and the return period is marked as submitted together with it.

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -F "file=@DE_VAT_2026-06_proof.pdf" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/submission-proof"

The request is multipart/form-data with exactly one file in the file field. The filename must carry its extension (for example proof.pdf).

The response is the return period in its new state:

{
  "id": 90417,
  "country": "DE",
  "returnType": "vat-return",
  "periodStart": "2026-06-01",
  "periodEnd": "2026-06-30",
  "status": "submitted",
  "amount": 18432.55,
  "currency": "EUR",
  "isNil": false,
  "submissionDeadline": "2026-08-10",
  "paymentDeadline": "2026-08-10"
}

The proof itself appears in the return period's document list as a proof-of-submission document, downloadable like any other — see Documents & error catalogue.

Two rules to be aware of:

  • The return has to be approved first. Recording a filing on a return that is not approved gets a 409. The order is always: review → approve → file → record.
  • The filing is locked to the submission file Taxually last produced. Submit the submission-file you downloaded from this return period; the recorded submission refers to that exact file.

What can go wrong

StatusWhenWhat to do
400No file, an empty file, a file that is neither a PDF nor an image, or a filename without an extension.Fix the upload and retry.
404No such return period for this company, or the company is not accessible to your credentials.Check the ids against GET /companies and the return period list.
409 — already submitted"This return period has already been submitted."Nothing — the filing is already recorded.
409 — no submission file yet"This return period has no completed return file to file yet. Wait for the return to be produced, then approve it." ("Return file" in the API's message means the submission file.)Poll the return period; approve once it reaches waiting-for-approval.
409 — wrong stateThe return is not approved, VAT-group returns are submitted the way the group is set up, or submitting is turned off by client configuration.Read the detail of the problem response; it says which.

All errors follow the problem-document format described in Errors & conventions.

Summary: the two endings

Taxually filesYou file
1Approve: POST …/approvalsApprove: POST …/approvals
2Poll until submittedDownload the submission-file document
3Download the proof from the document listSubmit it to the tax authority yourself
4—Record it: POST …/submission-proof with the proof

Either way, the return period ends at submitted, with a proof-of-submission document in its list.

Next steps

Documents & error catalogue

Taxually's output reaches you at two levels. Per return period: the documents — the submission file, the readable draft, the analytics workbook, and the proof of submission — listed first, then downloaded through short-lived links. Per uploaded file: the error catalogue, a spreadsheet of every error and warning found in that file during processing.

This page answers one question: how do you get Taxually's output back into your systems? It assumes a returnPeriodId from Returns, approval & submission and, for the error catalogue, a file id from the upload receipt or the data period status on Uploading data & processing.

All examples run against the demo company Bergmann Home & Living GmbH (companyId 48213) and its German VAT return for June 2026 (returnPeriodId 90417), using the API key header described in Authentication:

BASE="https://taxually-test-api.test.taxually.com"   # test
# Production: https://taxually-api.taxually.com

The two kinds of output

Processing a data period produces artifacts at two levels, and each has its own endpoints:

  • Per return period — the documents. Every return period carries a flat list of what it produced: the return file itself, a human-readable draft, the analytics workbook behind the numbers, any custom extra file, and — once filed — the proof of submission. You list them, then fetch a download link per document.
  • Per uploaded file — the error catalogue. Each transaction file you uploaded gets, if processing found anything wrong in it, a spreadsheet listing every error and warning row by row. This is the detail behind the errorCount and warningCount you saw when polling the data period.

Both use the same download pattern, so read the next section before either.

Neither the documents endpoint nor the error-catalogue endpoint streams file bytes. Each returns a small JSON object with the document's name and a URL to download it from:

{
  "name": "DE_VAT_2026-06.xml",
  "url": "https://storage.taxually.com/documents/…?sig=<short-lived-signed-token>"
}
FieldTypeMeaning
namestringThe name of the document, including its extension.
urlstringThe URL the document can be downloaded from.

The url expires shortly after it is issued. Follow it straight away — request the link, download the file, done. Never store the URL, put it in a queue, or hand it to a user to click later; by then it will be dead. When you need the file again, request a fresh link.

List a return period's documents

GET /api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/documents

One flat list of everything the return period produced. Each entry says what the document is and carries the id the download endpoint takes.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/documents"

For our German VAT return, after it has been submitted:

[
  {
    "id": "f4a6c8e0-2b4d-46f8-a0c2-e4f6a8c0e2b4",
    "name": "DE_VAT_2026-06.xml",
    "type": "submission-file"
  },
  {
    "id": "a1c3e5f7-3b5d-47a9-b2c4-d6e8f0a2c4e6",
    "name": "DE_VAT_2026-06_draft.pdf",
    "type": "draft"
  },
  {
    "id": "b2d4f6a8-4c6e-48b0-83d5-e7f9a1b3d5f7",
    "name": "DE_VAT_2026-06_analytics.xlsx",
    "type": "analytics"
  },
  {
    "id": "c3e5a7b9-5d7f-49c1-84e6-f8a0b2c4d6e8",
    "name": "DE_VAT_2026-06_proof.pdf",
    "type": "proof-of-submission"
  }
]
FieldTypeMeaning
idstring (uuid)The identifier of the document, as accepted by the download endpoint of the same return period.
namestringThe name of the document, including its extension.
typestringWhat the document is. One of the five types below.

Two things about this list are worth remembering:

  • The list grows with the return's lifecycle. A return period that has not produced anything yet lists nothing (200 with an empty array). Once the return is calculated, the submission file, draft, and analytics appear. The proof-of-submission appears only after the return period has been submitted — whether Taxually filed it or you recorded your own filing on Returns, approval & submission.
  • The documents belong to the submission file Taxually last produced. If the return is regenerated — after corrected data, for example — the list changes to the new documents. Don't cache document ids across a regeneration; list again.

Returns a 404 problem when no such return period exists for the company, or the company is inaccessible (the two cases are deliberately indistinguishable — see Errors & conventions).

Document types

The type field takes exactly these five values:

ValueMeaning
submission-fileThe return file itself, in the format the tax authority accepts.
draftThe readable draft of the return, as reviewed before approval.
analyticsThe workbook backing the return with the transactions it was calculated from.
proof-of-submissionThe evidence of the filing, stored when the return period was submitted.
customAn extra file produced alongside the return, for the countries and clients that need one.

Which type you want depends on who is asking. Reviewing before approval: the draft. Reconciling the numbers: the analytics workbook. Filing with the tax authority yourself: the submission-file. Auditing a past filing: the proof-of-submission.

Download a document

GET /api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/documents/{documentId}/download

Fetches a short-lived download link for one document from the list above.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/documents/f4a6c8e0-2b4d-46f8-a0c2-e4f6a8c0e2b4/download"
{
  "name": "DE_VAT_2026-06.xml",
  "url": "https://storage.taxually.com/documents/…?sig=<short-lived-signed-token>"
}

Then download the file from url immediately — the link expires shortly after it is issued.

The documentId has to be one of the documents listed on this very return period. An identifier that is not among them is reported as 404, whether or not it names a document somewhere else — there are no cross-company or cross-period lookups.

StatusWhy
404No such return period for the company, the return period has no such document, or the company is inaccessible.

A practical two-call pattern for "give me the draft of return 90417":

# 1. List the documents, pick the one with "type": "draft"
curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/documents"

# 2. Fetch its link and download straight away
curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/documents/a1c3e5f7-3b5d-47a9-b2c4-d6e8f0a2c4e6/download"

The error catalogue: per-file detail

GET /api/public/v1/companies/{companyId}/data-periods/{period}/transaction-files/{fileId}/error-catalogue

When a data period finishes processing, its status response tells you, per uploaded file, how many transactions were read and how many errors and warnings were found (see Uploading data & processing). The error catalogue is the row-by-row detail behind those counts: a spreadsheet, produced per transaction file during processing, listing the errors and warnings found in that file.

The fileId is the id the upload receipt returned — the same one the data period status repeats per file.

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files/3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c/error-catalogue"
{
  "name": "webshop_orders_2026-06_error_catalogue.xlsx",
  "url": "https://storage.taxually.com/documents/…?sig=<short-lived-signed-token>"
}

Same download pattern as above: follow url straight away; the link expires shortly after it is issued.

A catalogue only exists when processing found something. A file that processed cleanly — zero errors, zero warnings — has no catalogue, and the endpoint reports that as 404. So check the file's errorCount and warningCount on the data period status first, and only request the catalogue when either is above zero. A 404 on a file you know was processed is not a failure; it means there is nothing to report.

StatusWhy
400The period isn't yyyy-MM.
404No data period for that company and month, no such file in it, the file has no error catalogue, or the company is inaccessible.

When do you reach for the catalogue?

  • After a completed period with errorCount > 0 — the returns were generated from the transactions that did process, so read the catalogue to decide whether the rejected rows change anything before approving on Returns, approval & submission.
  • After a completed period with warnings only — the engine processed every row but flagged some for a human to glance at.
  • After a failed period — the catalogue of the failing file, if one was produced, tells you what to correct before uploading a fixed file and processing again.

Polling, not pushing

Nothing on this page notifies you. You know a return period has documents because you polled the data period to completed and then read the return period's status — the model today is polling, as described on Uploading data & processing. Webhooks that announce events like "return generated" are coming soon, but they are not available yet; don't design your integration around them.

What's next

API reference

The complete reference for the Taxually Public API v1: every endpoint, every parameter, every response shape. If you want the narrative version of the process first, start with the Overview or the Quickstart: generate your first tax return.

The API is organized around REST. It has predictable resource-oriented URLs, accepts JSON request bodies (file uploads use multipart/form-data), returns JSON responses, and uses standard HTTP response codes and verbs. Errors are RFC 7807 problem documents — see Errors & conventions for the full model.

Conventions used on this page

Base URL. All paths on this page are relative to the base URL. The API is available on both environments:

https://taxually-test-api.test.taxually.com

Production is https://taxually-api.taxually.com. Every endpoint lives under the prefix /api/public/v1.

Authentication. Every endpoint requires authentication. Samples on this page use an API key in the Authorization: Bearer header; a JWT in the same header also works. API keys are created on the company settings page in the Taxually UI. See Authentication for details. In the samples, $BASE is the base URL and $TAXUALLY_API_KEY is your key.

Authorization. Which companies you can act on follows entirely from your credentials. GET /api/public/v1/companies lists exactly the set of companies every other endpoint accepts an identifier for. A company you cannot access is reported as 404 — identical to one that does not exist. You never see a 403 for a foreign company; existence is deliberately not revealed.

Errors. All errors are application/problem+json documents with title, status, detail, and instance:

{
  "title": "Resource not found",
  "status": 404,
  "detail": "No company 48213 exists.",
  "instance": "/api/public/v1/companies/48213"
}

detail is always a human-readable, actionable sentence. Per-endpoint sections below list which status codes each endpoint returns; the status-to-title mapping and the file-setup errors extension are documented on Errors & conventions.

Sample data. Every sample uses the same demo company: Bergmann Home & Living GmbH (companyId 48213, a German company selling into DE, PL, and FR), its June 2026 data period, and the return periods that period produced. IDs are consistent across all documentation pages — a value you see here matches the same entity on Companies, Uploading data & processing, and the other guide pages.

Polling. Processing runs in the background; today you poll for status. Webhooks are coming soon but not available.

Endpoint index

#Method & pathWhat it does
1GET /api/public/v1/companiesList the companies you can act on
2GET /api/public/v1/companies/{companyId}Get one company
3GET /api/public/v1/companies/{companyId}/file-setupsList file setups
4POST /api/public/v1/companies/{companyId}/file-setupsCreate a file setup
5PUT /api/public/v1/companies/{companyId}/file-setups/{setupName}Replace a file setup
6GET /api/public/v1/mapping-propertiesList mapping properties
7GET /api/public/v1/companies/{companyId}/data-periodsList data periods
8GET /api/public/v1/companies/{companyId}/data-periods/{period}Get a data period's status
9POST /api/public/v1/companies/{companyId}/data-periods/{period}/transaction-filesUpload a transaction file
10DELETE /api/public/v1/companies/{companyId}/data-periods/{period}/transaction-files/{fileId}Delete a transaction file
11POST /api/public/v1/companies/{companyId}/data-periods/{period}/restartRestart a data period
12POST /api/public/v1/companies/{companyId}/data-periods/{period}/processingStart processing
13GET /api/public/v1/companies/{companyId}/return-periodsList return periods
14GET /api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}Get one return period
15POST /api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/approvalsApprove a return
16POST /api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/submission-proofRecord a filing you made yourself
17GET /api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/documentsList a return's documents
18GET /api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/documents/{documentId}/downloadGet a download link for one document
19GET /api/public/v1/companies/{companyId}/data-periods/{period}/transaction-files/{fileId}/error-catalogueGet a file's error catalogue link

The groups below follow the order of the process: discover your companies, teach the engine your file formats, upload a month's data, process it, review and approve the returns it produced, record the filing if you file yourself, and download the artifacts.

Companies

The company is the legal entity all data belongs to. It is set up once through the Taxually UI (registration and onboarding); after that, everything runs through the API. The id returned here is the companyId every other endpoint takes. See the Companies guide for the onboarding context.

GET /companies — list companies

The entry point of the API. After onboarding, call this to discover the companies your credentials can act on. The set follows entirely from your credentials; there is no parameter to widen it.

Parameters

NameInTypeRequiredDescription
limitqueryintegerNoMaximum companies to return. Between 1 and 100. Default 50.
offsetqueryintegerNoNumber of companies to skip. Default 0.

Request body: none.

Paging: start at offset 0 and keep advancing by limit until fewer than limit companies come back. Order (by name) is stable between calls, so no company is skipped or repeated while the set doesn't change.

Response 200 — array of company objects:

FieldTypeDescription
idintegerThe identifier of the company. This is the value to send as companyId on every other endpoint.
namestringThe registered legal name of the company.
countryCodestring, nullableTwo-letter code of the country the company is established in. Null when no establishment country is recorded.

Status codes

CodeWhen
200Success. An empty array when your credentials can act on no company, or when the page lies past the end.
400limit not between 1 and 100, or offset negative. Problem document.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies?limit=50&offset=0"

Sample response — 200

[
  {
    "id": 48213,
    "name": "Bergmann Home & Living GmbH",
    "countryCode": "DE"
  },
  {
    "id": 51877,
    "name": "Nordwind Outdoor GmbH",
    "countryCode": "DE"
  }
]

GET /companies/{companyId} — get one company

Fetch a single company you can act on — typically to confirm an identifier before wiring it into an integration. A company that does not exist and one you cannot access are reported identically as 404.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company.

Request body: none.

Response 200 — one company object (fields as in the list endpoint above).

Status codes

CodeWhen
200Success.
404The company does not exist, or you cannot access it.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213"

Sample response — 200

{
  "id": 48213,
  "name": "Bergmann Home & Living GmbH",
  "countryCode": "DE"
}

File setups

A file setup describes one shape of transaction file: how the engine reads it (date format, separators, filename pattern) and which of its columns carries which transaction property. This is step 2 of the flow, and it's optional — global mappings for common systems (Amazon, Shopify, SAP, Oracle, and others) exist out of the box, so you only need a file setup for your own custom formats. An uploaded file that matches no setup cannot be processed. See File setups & mapping for the guide.

GET /companies/{companyId}/file-setups — list file setups

See the file setups the company currently uses to read its transaction files, with their column mappings. Only the setups active right now are listed; superseded and deleted ones are not.

The list includes any setups Taxually configured for a specific accounting system (SAP, NetSuite, lookup-table). Those carry settings this API does not expose and cannot be replaced through it — but they are listed because they compete for the same uploaded files. Two setups whose detection columns both match a file make the choice arbitrary, so you need to see the whole set.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the file setups.

Request body: none.

Response 200 — array of file setup objects, ordered by name:

FieldTypeDescription
namestringThe name of the file setup, unique within the company. It is the identifier the setup is addressed by, matched case-insensitively.
dateFormatstring, nullableThe .NET date format the date columns are parsed with, e.g. yyyy-MM-dd. Absent when the setup does not set one.
decimalSeparatorstring, nullableThe single character separating decimals in numeric columns, e.g. .. Absent when not set.
thousandSeparatorstring, nullableThe single character grouping thousands, e.g. ,. Absent when not set.
fileNamePatternstring, nullableA regular expression matched against the name of an uploaded file. When set, only matching files are read with this setup. Absent when the setup is selected by its columns alone.
columnMappingsarrayWhich column of the file carries which transaction property. Fields below.

Each entry of columnMappings:

FieldTypeDescription
fieldNamestringThe column header as it appears in your file, e.g. invoice_date. Matched case-insensitively, unique within the setup.
propertyNamestringThe transaction property the column is mapped onto, e.g. TransactionDate. One of the names the mapping-properties endpoint lists.
requiredbooleanWhether a row that leaves this column empty is rejected. Only applies to files that carry the column at all.
requiredForVatDetectionbooleanWhether this column has to be present for the file to be recognized as carrying VAT transactions. At least one column of a setup normally carries this flag, or no uploaded file will ever match the setup.
requiredForIntrastatDetectionbooleanWhether this column has to be present for the file to be recognized as carrying Intrastat data.
requiredForGlDetectionbooleanWhether this column has to be present for the file to be recognized as carrying general ledger data.

Status codes

CodeWhen
200Success. An empty array when the company has no file setups.
404The company does not exist, or you cannot access it.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/file-setups"

Sample response — 200

[
  {
    "name": "Webshop CSV",
    "dateFormat": "yyyy-MM-dd",
    "decimalSeparator": ".",
    "thousandSeparator": ",",
    "fileNamePattern": "^webshop_orders_.*\\.csv$",
    "columnMappings": [
      {
        "fieldName": "invoice_date",
        "propertyName": "TransactionDate",
        "required": true,
        "requiredForVatDetection": true,
        "requiredForIntrastatDetection": false,
        "requiredForGlDetection": false
      }
    ]
  }
]

POST /companies/{companyId}/file-setups — create a file setup

Teach the engine to read a custom file format. Flag at least one column as required for VAT detection, and pick a column every file of this shape carries — that is how an uploaded file is recognized as belonging to this setup. A setup that no file ever matches is useless — files meant for it fail processing because their format cannot be determined. Where several setups could match the same columns, set a fileNamePattern on each so the choice between them is not arbitrary.

Send the setup in one complete call rather than building it up over several. Every write clones the whole mapping configuration into a fresh version and marks the company's mappings as changed, which makes already-processed data periods eligible for reprocessing. A run of small writes leaves a trail of versions behind it, and a file uploaded mid-trail is read under whichever version happens to be current — an intermediate one whose detection column the file does not carry rejects it outright.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company the file setup belongs to.

Request body — JSON, the complete file setup. The same body shape creates and replaces a setup; both take the whole setup, never a patch.

FieldTypeRequiredDescription
namestringYesThe name of the file setup, unique within the company, at most 30 characters.
dateFormatstringNo.NET date format for the file's date columns, e.g. yyyy-MM-dd. Use lowercase y and d specifiers; YY and D are not valid. Leave absent to let the engine parse dates without a fixed format.
decimalSeparatorstringNoThe single character separating decimals, e.g. ..
thousandSeparatorstringNoThe single character grouping thousands, e.g. ,.
fileNamePatternstringNoRegex matched against uploaded file names, at most 50 characters. Set it when several setups would otherwise match the same columns.
columnMappingsarrayYesAt least one mapping. No two mappings may name the same column.

Each entry of columnMappings:

FieldTypeRequiredDescription
fieldNamestringYesThe column header in your file, at most 100 characters, unique within the request, matched case-insensitively.
propertyNamestringYesOne of the names from GET /mapping-properties. Case-sensitive.
requiredbooleanNoReject rows that leave this column empty.
requiredForVatDetectionbooleanNoColumn must be present for VAT detection.
requiredForIntrastatDetectionbooleanNoColumn must be present for Intrastat detection.
requiredForGlDetectionbooleanNoColumn must be present for general ledger detection.

Response 201 — the setup as stored (fields as in the list endpoint). The Location header points at the stored setup.

Status codes

CodeWhen
201The setup was stored.
400The setup was rejected and nothing was stored. The problem document carries an errors member listing every reason at once — see Errors & conventions.
404The company does not exist, or you cannot access it.
409The company already has a file setup with this name. detail: "A file setup named 'Webshop CSV' already exists for company 48213. Replace it instead of creating it again."

Sample request

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Webshop CSV",
    "dateFormat": "yyyy-MM-dd",
    "decimalSeparator": ".",
    "thousandSeparator": ",",
    "fileNamePattern": "^webshop_orders_.*\\.csv$",
    "columnMappings": [
      {
        "fieldName": "invoice_date",
        "propertyName": "TransactionDate",
        "required": true,
        "requiredForVatDetection": true,
        "requiredForIntrastatDetection": false,
        "requiredForGlDetection": false
      }
    ]
  }' \
  "$BASE/api/public/v1/companies/48213/file-setups"

Sample response — 201 (Location: /api/public/v1/companies/48213/file-setups/Webshop%20CSV)

{
  "name": "Webshop CSV",
  "dateFormat": "yyyy-MM-dd",
  "decimalSeparator": ".",
  "thousandSeparator": ",",
  "fileNamePattern": "^webshop_orders_.*\\.csv$",
  "columnMappings": [
    {
      "fieldName": "invoice_date",
      "propertyName": "TransactionDate",
      "required": true,
      "requiredForVatDetection": true,
      "requiredForIntrastatDetection": false,
      "requiredForGlDetection": false
    }
  ]
}

Sample error — 400 (every reason at once; columnMappingIndex is the zero-based position in your columnMappings array, null when the message concerns the setup as a whole)

{
  "title": "Invalid request",
  "status": 400,
  "detail": "The file setup was rejected and nothing was stored. Every reason is listed in 'errors'.",
  "instance": "/api/public/v1/companies/48213/file-setups",
  "errors": [
    {
      "message": "'transactionDate' is not a transaction property a column can be mapped onto. The mapping properties endpoint lists the accepted names; they are case-sensitive.",
      "columnMappingIndex": 0
    },
    {
      "message": "dateFormat is not a valid .NET date format: 'YY' and 'YYYY' are not year specifiers and 'D' and 'DD' are not day specifiers. Use lowercase 'yy'/'yyyy' for the year and 'd'/'dd' for the day, for example 'yyyy-MM-dd'.",
      "columnMappingIndex": null
    }
  ]
}

PUT /companies/{companyId}/file-setups/{setupName} — replace a file setup

Change how a custom format is read. This is a wholesale replacement, not a patch: a column mapping the request no longer names is removed from the setup, and a setting left out is cleared. Read the setup, change what needs changing, send the whole thing back. Replacing in one call is deliberately the only shape on offer — the same versioning rationale as create applies: batch your changes into one call.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the file setup.
setupNamepathstringYesThe name of the file setup to replace. Matched case-insensitively.

Request body — same shape as create. name is optional when replacing; if sent, it has to match the name in the URL. The stored spelling of the name wins — replacing a setup cannot quietly recase its name.

Response 200 — the setup as stored.

Status codes

CodeWhen
200The setup was replaced.
400Rejected; the errors member lists every reason. Also returned when the body's name differs from the name in the URL.
404The company has no setup with this name, or you cannot access the company.
409The setup is one Taxually configured for a specific accounting system and cannot be replaced through this API. detail tells you to ask Taxually to change it.

Sample request

curl -X PUT \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "dateFormat": "yyyy-MM-dd",
    "decimalSeparator": ".",
    "thousandSeparator": ",",
    "fileNamePattern": "^webshop_orders_.*\\.csv$",
    "columnMappings": [
      {
        "fieldName": "invoice_date",
        "propertyName": "TransactionDate",
        "required": true,
        "requiredForVatDetection": true,
        "requiredForIntrastatDetection": false,
        "requiredForGlDetection": false
      }
    ]
  }' \
  "$BASE/api/public/v1/companies/48213/file-setups/Webshop%20CSV"

Sample response — 200 (the setup as stored)

{
  "name": "Webshop CSV",
  "dateFormat": "yyyy-MM-dd",
  "decimalSeparator": ".",
  "thousandSeparator": ",",
  "fileNamePattern": "^webshop_orders_.*\\.csv$",
  "columnMappings": [
    {
      "fieldName": "invoice_date",
      "propertyName": "TransactionDate",
      "required": true,
      "requiredForVatDetection": true,
      "requiredForIntrastatDetection": false,
      "requiredForGlDetection": false
    }
  ]
}

Mapping properties

GET /mapping-properties — list mapping properties

The reference list to consult before creating or replacing a file setup: these names are the values to send as propertyName, spelled exactly as listed — they are case-sensitive, and a name not on this list is rejected. This is global reference data: it takes no company and performs no company access check, but authentication is still required.

This list does not tell you which properties your setup must map — that follows from the setup's own detection flags and from what your data has to support.

Parameters: none. Request body: none.

Response 200 — array, ordered by name:

FieldTypeDescription
namestringThe name to send as propertyName in a file setup's column mappings. Case-sensitive.
typestringWhat the mapped column has to contain: date, decimal, integer, boolean or text. How a date or number is written in the file (format, separators) is a property of the file setup, not of the property.

Status codes

CodeWhen
200Success.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/mapping-properties"

Sample response — 200

[
  { "name": "TransactionDate", "type": "date" }
]

(Sample truncated to one property for illustration — the live endpoint returns the complete list.)

Data periods & transaction files

A data period is a month (yyyy-MM) open for uploading transaction data. Which months are open is driven by the company's VAT number validity and its subscriptions — one data period per company per month. The data period carries the uploaded transaction files and a processing state. This is steps 3 and 4 of the flow: find your upload window, then get the month's files in. See Uploading data & processing for the guide.

GET /companies/{companyId}/data-periods — list data periods

Find your upload window. This list carries only the period and its processing state; request an individual data period for per-file statistics and failure reasons. Newest period first.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the data periods.

Request body: none.

Response 200 — array:

FieldTypeDescription
periodstringThe data period, in yyyy-MM format.
statusstringProcessing state: new (ready to receive data, nothing processed yet), processing (keep polling), completed (calculated; returns available), or error (processing failed; you have to act).

Status codes

CodeWhen
200Success. An empty array when the company has no data periods.
404The company does not exist, or you cannot access it.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods"

Sample response — 200

[
  { "period": "2026-07", "status": "new" },
  { "period": "2026-06", "status": "completed" },
  { "period": "2026-05", "status": "completed" }
]

GET /companies/{companyId}/data-periods/{period} — get a data period's status

The polling target after starting processing — the processing endpoint's Location header points here. Reports the period's processing state, the failure reason when it errored, and per-file statistics: transactions read, errors and warnings found per uploaded file.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the data period.
periodpathstringYesThe data period in yyyy-MM format, for example 2026-07.

Request body: none.

Response 200:

FieldTypeDescription
periodstringThe data period, in yyyy-MM format.
statusstringnew, processing, completed or error.
errorstring, nullableA short, human-readable reason for the failure. Only present when status is error; omitted otherwise.
filesarrayThe transaction files that were processed for this data period. Empty when the period has none. Fields below.

Each entry of files:

FieldTypeDescription
idstring (uuid)The document identifier of the uploaded transaction file — the same id the upload receipt returned, and the one the error-catalogue endpoint takes.
namestringThe original name of the uploaded transaction file.
transactionCountintegerThe number of transactions read from the file.
errorCountintegerThe number of processing errors found in the file.
warningCountintegerThe number of processing warnings found in the file.

Status codes

CodeWhen
200Success.
400period is not in yyyy-MM format.
404No data period exists for that company and period, or you cannot access the company.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06"

Sample response — 200 (processing finished)

{
  "period": "2026-06",
  "status": "completed",
  "files": [
    {
      "id": "8c2f4a6e-1b3d-45f7-9a8c-2e4d6f8a0b1c",
      "name": "amazon_de_2026-06.csv",
      "transactionCount": 18452,
      "errorCount": 0,
      "warningCount": 37
    },
    {
      "id": "3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c",
      "name": "webshop_orders_2026-06.csv",
      "transactionCount": 2311,
      "errorCount": 3,
      "warningCount": 12
    }
  ]
}

Sample response — 200 (failed period, error present)

{
  "period": "2026-06",
  "status": "error",
  "error": "Transaction file processing failed.",
  "files": [
    {
      "id": "3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c",
      "name": "webshop_orders_2026-06.csv",
      "transactionCount": 0,
      "errorCount": 1,
      "warningCount": 0
    }
  ]
}

POST /companies/{companyId}/data-periods/{period}/transaction-files — upload a transaction file

Get the month's data to Taxually, one call per file. The file is stored for later processing; this endpoint does not start anything, so you can upload several files before requesting processing.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the data period.
periodpathstringYesThe data period in yyyy-MM format, for example 2026-07.

Request body — multipart/form-data carrying exactly one file:

FieldTypeRequiredDescription
filebinaryYesThe transaction file to upload.

Response 201 — a receipt. Processing statistics are not part of it; the file has not been processed yet. The Location header points at the data period status resource.

FieldTypeDescription
idstring (uuid)The document identifier of the uploaded transaction file. Keep it — the error-catalogue endpoint takes it.
namestringThe original name of the uploaded transaction file.

Status codes

CodeWhen
201The file was accepted and stored.
400period not in yyyy-MM format, the request is not multipart, or the file is empty.
404No data period exists for that company and period, or you cannot access the company.
409The period is currently being processed, or a file with the same name already exists in it. detail: "A file named 'webshop_orders_2026-06.csv' already exists in 2026-06. Delete it or upload under a different name."

Sample request

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -F "file=@amazon_de_2026-06.csv" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files"

Sample response — 201 (Location: /api/public/v1/companies/48213/data-periods/2026-06)

{
  "id": "8c2f4a6e-1b3d-45f7-9a8c-2e4d6f8a0b1c",
  "name": "amazon_de_2026-06.csv"
}

DELETE /companies/{companyId}/data-periods/{period}/transaction-files/{fileId} — delete a transaction file

Removes a single transaction file from the data period so that a corrected file can be uploaded in its place. The file is retired rather than erased: it stops counting towards the period and disappears from the data period status, but it stays on record — and its name is released, so the corrected file can be uploaded under the same name. Deleting a file does not reprocess the period; request processing afterwards.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the data period.
periodpathstringYesThe data period in yyyy-MM format, for example 2026-07.
fileIdpathstring (uuid)YesThe identifier the transaction file was given when it was uploaded — the id from the upload receipt, also listed per file on the data period status.

Request body: none.

Response 204 — no body. The file no longer counts towards the period.

Status codes

CodeWhen
204The file was removed from the period.
400period not in yyyy-MM format.
404No data period exists for that company and period, the period holds no file with that fileId, or you cannot access the company.
409The period is currently being processed, so its files cannot be changed.

Sample request

curl -X DELETE \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files/3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c"

Sample response — 204

HTTP/1.1 204 No Content

(No response body.)

POST /companies/{companyId}/data-periods/{period}/restart — restart a data period

Restarts the data period, returning it to the state it was in before any data was uploaded. Every transaction file uploaded into the period is discarded: the period comes back empty, and all of its files have to be uploaded again before it can be processed. Use it to recover a period whose data is wrong as a whole; when only one file is bad, delete that file instead, which leaves the period's other files untouched.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the data period.
periodpathstringYesThe data period in yyyy-MM format, for example 2026-07.

Request body: none.

Response 200 — the period's state after the restart, in the same shape as the data period status endpoint, with an empty file list.

Status codes

CodeWhen
200The period was restarted; the body is its post-restart state.
400period not in yyyy-MM format.
404No data period exists for that company and period, or you cannot access the company.
409The period is currently being processed, so it cannot be restarted.

Sample request

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/restart"

Sample response — 200

{
  "period": "2026-06",
  "status": "new",
  "files": []
}

Processing

POST /companies/{companyId}/data-periods/{period}/processing — start processing

Step 5 of the flow. Once all files for the month are uploaded, one call starts processing. Processing always runs through to return generation — there is no separate finalization step in the public API. It continues in the background after the response; poll the data period status resource the Location header points at. Polling is the model today; webhooks are coming soon.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the data period.
periodpathstringYesThe data period in yyyy-MM format, for example 2026-07.

Request body: none.

Response 202 — no body. The Location header points at the status resource to poll.

Status codes

CodeWhen
202Processing was started. Poll the Location resource.
400period not in yyyy-MM format.
404No data period exists for that company and period, or you cannot access the company.
409The period is already processing ("The period is already processing."), holds no files to process ("There are no files to process in 2026-06. Upload transaction files first."), or holds a file that failed to process ("A file in 2026-06 failed to process. Delete it and upload a corrected file before processing the period.").

Sample request

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/processing"

Sample response — 202

HTTP/1.1 202 Accepted
Location: /api/public/v1/companies/48213/data-periods/2026-06

(No response body.)

Return periods

A return period is a specific tax return within a period: one jurisdiction, one return type, one period — for example, the German VAT return for June 2026. One data period can produce several return periods (a German VAT return, a Polish VAT return, and a German ESL for the same June). This is step 7 of the flow: after processing completes, see what it produced. See Returns, approval & submission for the guide.

A return period moves through six states, in lifecycle order: new (exists, no return calculated yet), in-progress (being worked on — keep polling), waiting-for-approval (calculated, waiting to be approved; also reported while waiting for a required second approval), approved (waiting to be submitted), submitted (filed with the tax authority), and error (producing the return failed; Taxually resolves these — you cannot act on them).

GET /companies/{companyId}/return-periods — list return periods

See the returns processing produced. The whole history is returned; the list is not paged. Both date filters match against the start of the period the return covers, not against its deadlines. Newest first.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the return periods.
dateFromquerystringNoOnly list returns whose period starts on or after this date, yyyy-MM-dd.
dateToquerystringNoOnly list returns whose period starts on or before this date, yyyy-MM-dd.
statusquerystringNoOnly list returns in this state. One of new, in-progress, waiting-for-approval, approved, submitted, error.

Request body: none.

Response 200 — array of return period objects:

FieldTypeDescription
idintegerThe identifier of the return period.
countrystringThe code of the jurisdiction the return is filed in, for example CZ.
returnTypestringThe kind of return, for example vat-return, esl or intrastat-arrival. The full list is under Return types on Errors & conventions; treat unrecognized values (including unknown) as valid.
periodStartdate, nullableThe first day of the period the return covers.
periodEnddate, nullableThe last day of the period the return covers.
statusstringOne of the six lifecycle states above.
errorstring, nullableWhy producing the return failed — only present when status is error and the reason is one you can be told. Omitted otherwise; Taxually resolves these failures.
amountnumber, nullableThe VAT amount of the return, in currency. Null while not calculated, and for return types carrying no such figure, such as listings.
currencystring, nullableThe code of the currency the return is reported in, for example CZK. Null when it cannot be resolved.
isNilboolean, nullableWhether the return reports nothing for the period. Null while not calculated.
submissionDeadlinedate, nullableThe date the return has to be submitted to the tax authority by. Null when the entry has no submission deadline of its own — a standalone VAT payment (returnType vat-payment), for example, carries only a payment deadline.
paymentDeadlinedate, nullableThe date the amount owed has to be paid by. Null when the return is not payable — nothing owed, or the return type carries no payment.

(error is omitted from all three sample entries because it is null — the field is only serialized when set.)

Status codes

CodeWhen
200Success. An empty array when nothing matches.
400A filter is not in the expected format, or status is not one the API reports. detail lists all six accepted values.
404The company does not exist, or you cannot access it.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods?dateFrom=2026-06-01&dateTo=2026-06-30"

Sample response — 200

[
  {
    "id": 90417,
    "country": "DE",
    "returnType": "vat-return",
    "periodStart": "2026-06-01",
    "periodEnd": "2026-06-30",
    "status": "waiting-for-approval",
    "amount": 18432.55,
    "currency": "EUR",
    "isNil": false,
    "submissionDeadline": "2026-08-10",
    "paymentDeadline": "2026-08-10"
  },
  {
    "id": 90418,
    "country": "PL",
    "returnType": "vat-return",
    "periodStart": "2026-06-01",
    "periodEnd": "2026-06-30",
    "status": "waiting-for-approval",
    "amount": 9214.30,
    "currency": "PLN",
    "isNil": false,
    "submissionDeadline": "2026-07-27",
    "paymentDeadline": "2026-07-27"
  },
  {
    "id": 90419,
    "country": "DE",
    "returnType": "esl",
    "periodStart": "2026-06-01",
    "periodEnd": "2026-06-30",
    "status": "waiting-for-approval",
    "amount": null,
    "currency": "EUR",
    "isNil": false,
    "submissionDeadline": "2026-07-27",
    "paymentDeadline": null
  }
]

GET /companies/{companyId}/return-periods/{returnPeriodId} — get one return period

Check the state of a specific return — the natural polling target between "processing completed" and waiting-for-approval, and again after approval and submission. A return period that does not exist for this company, including one belonging to another company, is reported as 404.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the return period.
returnPeriodIdpathintegerYesThe identifier of the return period.

Request body: none.

Response 200 — one return period object (fields as in the list endpoint).

Status codes

CodeWhen
200Success.
404No such return period exists for this company, or you cannot access the company.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417"

Sample response — 200

{
  "id": 90417,
  "country": "DE",
  "returnType": "vat-return",
  "periodStart": "2026-06-01",
  "periodEnd": "2026-06-30",
  "status": "waiting-for-approval",
  "amount": 18432.55,
  "currency": "EUR",
  "isNil": false,
  "submissionDeadline": "2026-08-10",
  "paymentDeadline": "2026-08-10"
}

Approvals

POST /companies/{companyId}/return-periods/{returnPeriodId}/approvals — approve a return

Step 8 of the flow: your sign-off that the return should be submitted. One call approves. Only a return in waiting-for-approval can be approved.

A company can be set up to require two approvals on returns that have a VAT amount (a non-null amount). The return then keeps reporting waiting-for-approval until a second, different user approves it — a repeat call by the same user is refused with 409.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the return period.
returnPeriodIdpathintegerYesThe identifier of the return period to approve.

Request body: none.

Response 201 — the receipt. The Location header points at the return period.

FieldTypeDescription
approvedAtstring (date-time)The moment the approval was recorded, in UTC.

Status codes

CodeWhen
201The approval was recorded.
404No such return period exists for this company, or you cannot access the company.
409The return period is not waiting for approval (only waiting-for-approval returns can be approved; VAT-group returns are approved in the order the group is set up for), or you already recorded the first of two approvals: "This return period was already approved by you; a second approval must come from a different user."

Sample request

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/approvals"

Sample response — 201 (Location: /api/public/v1/companies/48213/return-periods/90417)

{
  "approvedAt": "2026-07-14T09:32:11Z"
}

Submission proof

POST /companies/{companyId}/return-periods/{returnPeriodId}/submission-proof — record a filing you made yourself

The alternative ending of the flow — only for partners who file with the tax authority themselves. This records a filing you have already made — it does not ask Taxually to file anything. The proof document is therefore required: the return period is only marked as submitted together with the evidence of the filing.

The return has to be approved first, and it is filed against the submission file Taxually last produced for it — the file the submission is then locked to. Skip this endpoint entirely when Taxually does the submission; in that case Taxually stores the proof.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the return period.
returnPeriodIdpathintegerYesThe identifier of the return period that was filed.

Request body — multipart/form-data carrying exactly one file:

FieldTypeRequiredDescription
filebinaryYesThe proof of submission the tax authority issued: a PDF or an image. The filename must carry its extension, e.g. proof.pdf.

Response 200 — the return period in its new state (fields as in the return periods list endpoint), now "status": "submitted".

Status codes

CodeWhen
200The filing was recorded; the return period is now submitted.
400No file, an empty file, a file that is neither a PDF nor an image, or a filename without an extension.
404No such return period exists for this company, or you cannot access the company.
409Already submitted ("This return period has already been submitted."), no completed return file to file yet ("This return period has no completed return file to file yet. Wait for the return to be produced, then approve it."), or the return cannot be submitted in its state — a return has to be approved before its filing can be recorded, VAT-group returns are submitted the way the group is set up, and submitting can be turned off by client configuration.

Sample request

curl -X POST \
  -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  -F "file=@DE_VAT_2026-06_proof.pdf" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/submission-proof"

Sample response — 200

{
  "id": 90417,
  "country": "DE",
  "returnType": "vat-return",
  "periodStart": "2026-06-01",
  "periodEnd": "2026-06-30",
  "status": "submitted",
  "amount": 18432.55,
  "currency": "EUR",
  "isNil": false,
  "submissionDeadline": "2026-08-10",
  "paymentDeadline": "2026-08-10"
}

Documents

The final step of the flow: download the artifacts a return period produced — the submission file (the return itself, in the tax authority's format), the human-readable draft, the analytics workbook, any custom extra file, and, once filed, the proof of submission. Documents are downloaded via short-lived links. See Documents & error catalogue for the guide and the full document type list.

GET /companies/{companyId}/return-periods/{returnPeriodId}/documents — list a return's documents

One flat list of everything the return period produced. Each entry reports what it is, and its id is the one the download endpoint takes. The documents belong to the submission file Taxually last produced for the period, so the list changes when the return is regenerated. A return period that has not produced anything yet lists nothing.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the return period.
returnPeriodIdpathintegerYesThe identifier of the return period.

Request body: none.

Response 200 — array:

FieldTypeDescription
idstring (uuid)The identifier of the document, as accepted by the download endpoint of the same return period.
namestringThe name of the document, including its extension.
typestringWhat the document is: submission-file (the return file, in the format the tax authority accepts), draft (the readable draft, as reviewed before approval), analytics (the workbook backing the return with the transactions it was calculated from), proof-of-submission (the evidence of the filing, stored when the return period was submitted), or custom (an extra file produced alongside the return, for the countries and clients that need one).

Status codes

CodeWhen
200Success. An empty array when the return period has not produced anything yet.
404No such return period exists for this company, or you cannot access the company.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/documents"

Sample response — 200 (after submission)

[
  {
    "id": "f4a6c8e0-2b4d-46f8-a0c2-e4f6a8c0e2b4",
    "name": "DE_VAT_2026-06.xml",
    "type": "submission-file"
  },
  {
    "id": "a1c3e5f7-3b5d-47a9-b2c4-d6e8f0a2c4e6",
    "name": "DE_VAT_2026-06_draft.pdf",
    "type": "draft"
  },
  {
    "id": "b2d4f6a8-4c6e-48b0-83d5-e7f9a1b3d5f7",
    "name": "DE_VAT_2026-06_analytics.xlsx",
    "type": "analytics"
  },
  {
    "id": "c3e5a7b9-5d7f-49c1-84e6-f8a0b2c4d6e8",
    "name": "DE_VAT_2026-06_proof.pdf",
    "type": "proof-of-submission"
  }
]

GET /companies/{companyId}/return-periods/{returnPeriodId}/documents/{documentId}/download — get a download link for one document

The final step of the flow: fetch a short-lived link to one document listed on the return period. The document has to be one of the documents listed on this very return period; an identifier that is not among them is reported as missing whether or not it names a document elsewhere — there are no cross-company or cross-period lookups.

The returned link expires shortly after it is issued. Download straight away; never store the URL.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the return period.
returnPeriodIdpathintegerYesThe identifier of the return period the document belongs to.
documentIdpathstring (uuid)YesThe identifier of the document, as listed on the return period.

Request body: none.

Response 200:

FieldTypeDescription
namestringThe name of the document, including its extension.
urlstringThe URL the document can be downloaded from. Short-lived — follow it straight away rather than storing it.

Status codes

CodeWhen
200Success.
404No such return period, the return period has no such document, or you cannot access the company.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/return-periods/90417/documents/f4a6c8e0-2b4d-46f8-a0c2-e4f6a8c0e2b4/download"

Sample response — 200

{
  "name": "DE_VAT_2026-06.xml",
  "url": "https://storage.taxually.com/documents/…?sig=<short-lived-signed-token>"
}

Error catalogue

GET /companies/{companyId}/data-periods/{period}/transaction-files/{fileId}/error-catalogue — get a file's error catalogue link

The per-file feedback loop after processing. The error catalogue is a spreadsheet, produced per transaction file during processing, listing the errors and warnings found in that file. Use the per-file errorCount and warningCount on the data period status to decide which files to fetch a catalogue for.

A catalogue only exists once the file has been processed and something was found in it — a file that processed cleanly has none, which is reported as 404. The returned link expires shortly after it is issued; download straight away, never store the URL.

Parameters

NameInTypeRequiredDescription
companyIdpathintegerYesThe identifier of the company that owns the data period.
periodpathstringYesThe data period in yyyy-MM format.
fileIdpathstring (uuid)YesThe identifier the transaction file was given when it was uploaded — from the upload receipt or the data period status.

Request body: none.

Response 200:

FieldTypeDescription
namestringThe name of the document, including its extension.
urlstringThe URL the document can be downloaded from. Short-lived — follow it straight away rather than storing it.

Status codes

CodeWhen
200Success.
400period is not in yyyy-MM format.
404No data period, no such file in it, the file has no error catalogue (it processed cleanly), or you cannot access the company.

Sample request

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files/3e5a7c9b-2d4f-46a8-b1c3-5e7f9a1b3d5c/error-catalogue"

Sample response — 200

{
  "name": "webshop_orders_2026-06_error_catalogue.xlsx",
  "url": "https://storage.taxually.com/documents/…?sig=<short-lived-signed-token>"
}

See also

Errors & conventions

Every response the Taxually Public API sends follows the same small set of rules. This page collects them in one place: how errors are shaped, what each status code means and what to do about it, how retries behave, how the API is versioned, and how to poll politely while a return is being generated. Read it once before you build; link your error handler to it forever.

For the step-by-step flow these conventions apply to, see Quickstart: generate your first tax return. For how to authenticate the calls in the first place, see Authentication.

Request and response conventions

  • Base URLs. The API is available on both environments: https://taxually-test-api.test.taxually.com on test, https://taxually-api.taxually.com in production. Samples throughout the docs write the base URL as $BASE.
  • Rate limits. No rate limits are currently published for this API. Build a polite client anyway: poll at the intervals recommended under Polling etiquette and back off on repeated identical responses. If your integration will make sustained high-volume calls, talk to your Taxually representative first.
  • Path prefix. Every endpoint lives under /api/public/v1 (see Versioning below).
  • Formats. Requests that carry a body send JSON, except the two file uploads (transaction files and submission proof), which send multipart/form-data. Successful responses are application/json. Errors are application/problem+json.
  • Authentication. Every endpoint requires credentials — either an API key or a JWT, both sent in the Authorization: Bearer header. Details on Authentication.
  • Write receipts. Creating something returns 201 Created with a Location header pointing at the resource to check next. Starting processing returns 202 Accepted with no body — the Location header points at the data period status resource to poll.
  • Download links expire. The error-catalogue and document-download endpoints return a short-lived URL. Follow it straight away; never store it. When you need the file again, ask the endpoint for a fresh link. See Documents & error catalogue.
  • Omitted nulls. Some nullable fields are omitted from the JSON entirely when they have no value — for example, the error field on a data period or return period only appears when the status is error. Parse defensively: treat a missing field and a null field the same way.

Versioning

The version is in the path: /api/public/v1/.... There is no version header and no version picker — the URL you call is the version you get.

v1 is the current and only version. When a breaking change is ever needed, it will ship under a new path segment; v1 will not change shape underneath you. New fields may still be added to responses within v1, so build a tolerant parser: ignore fields you do not recognize rather than rejecting them.

The error format

Every error is an RFC 7807 problem document — Content-Type: application/problem+json — with the standard members:

MemberTypeMeaning
titlestringA short, fixed name for the class of error (see the status code table — each status has a fixed title).
statusintegerThe HTTP status code, repeated in the body.
detailstringA human-readable, actionable sentence about this specific failure. This is the field to read, log, and show.
instancestringThe path of the request that failed.
typestringMay be absent.

All members are nullable — key your error handling on status and read detail for the reason, rather than assuming every member is present.

A real example. Asking for a company your credentials cannot see (or one that does not exist — the API deliberately does not tell you which, see 404 below):

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213"
{
  "title": "Resource not found",
  "status": 404,
  "detail": "No company 48213 exists.",
  "instance": "/api/public/v1/companies/48213"
}

detail is always a complete sentence you can act on. The API words every rejection — you should rarely need to guess why a call failed.

Validation errors on file setups

One endpoint family extends the problem document. When a file setup is rejected (see File setups & mapping), the 400 response carries an errors extension member listing every problem at once, so you can fix the whole setup in one pass instead of resubmitting to discover the next issue:

{
  "title": "Invalid request",
  "status": 400,
  "detail": "The file setup was rejected and nothing was stored. Every reason is listed in 'errors'.",
  "instance": "/api/public/v1/companies/48213/file-setups",
  "errors": [
    {
      "message": "'transactionDate' is not a transaction property a column can be mapped onto. The mapping properties endpoint lists the accepted names; they are case-sensitive.",
      "columnMappingIndex": 0
    },
    {
      "message": "dateFormat is not a valid .NET date format: 'YY' and 'YYYY' are not year specifiers and 'D' and 'DD' are not day specifiers. Use lowercase 'yy'/'yyyy' for the year and 'd'/'dd' for the day, for example 'yyyy-MM-dd'.",
      "columnMappingIndex": null
    }
  ]
}
  • errors[].message — what is wrong, in words you can act on.
  • errors[].columnMappingIndex — the zero-based position of the offending entry in the columnMappings array you submitted; null when the message concerns the setup as a whole.

A rejected setup stores nothing. Fix every listed reason and send the complete setup again.

Status codes and what to do about each

Each status code carries a fixed title, so you can branch on either.

StatustitleWhen you see itWhat to do
200—A read succeeded, or a replace/submission returned the resource's new state.Carry on.
201—Something was created (a file uploaded, a setup stored, an approval recorded).Follow the Location header for the resource to check next.
202—Processing was accepted and runs in the background.Poll the resource the Location header points at — see Polling etiquette.
400Invalid requestA parameter or payload was rejected — a period not in yyyy-MM format, a status filter value the API does not report, a rejected file setup.Read detail (and errors, if present), fix the request, send it again. Do not retry unchanged — the same request gets the same answer.
400Invalid request bodyThe body is not parseable JSON.Fix the JSON and resend.
401Not authenticatedMissing or invalid credentials.Check the credential and the header it is sent in — see Authentication. Do not retry until the credential is fixed.
403Not allowedAuthenticated, but this action is forbidden.You will not see 403 for company access (that is a 404, below). Read detail; if it does not resolve the question, contact your Taxually representative.
404Resource not foundThe resource does not exist — or it belongs to a company your credentials cannot access. The two cases are deliberately indistinguishable.First check the identifier against GET /api/public/v1/companies — that list is exactly the set of companies your credentials can act on. A company missing from it will 404 on every endpoint, whether or not it exists. Also remember: an error catalogue 404s when the file processed cleanly — for that endpoint, 404 can be good news.
409ConflictThe request clashes with the current state of the resource.Read detail — it names the conflict and what to do about it. Then GET the resource to see its actual state before acting again. See the examples below.
422Request cannot be processedThe request is well-formed but breaks a business rule. No v1 endpoint currently documents a specific 422 case; the status is reserved for such rejections.Read detail and change what you are asking for; resending the same request will not help.

What 409 looks like in practice

409 is the API telling you the resource is not in the state your request assumes. Real detail messages from the flow:

  • Uploading a file with a name the period already holds: "A file named 'webshop_orders_2026-06.csv' already exists in 2026-06. Delete it or upload under a different name."
  • Starting processing while it is already running: "The period is already processing."
  • Starting processing with nothing uploaded: "There are no files to process in 2026-06. Upload transaction files first."
  • Approving a return you already approved, when the company requires two approvals: "This return period was already approved by you; a second approval must come from a different user."
  • Recording a filing twice: "This return period has already been submitted."

In every case the fix is the same: read detail, GET the resource, and let its actual state drive your next call.

Return types

The returnType field of a return period (see Returns, approval & submission) reports the kind of return, in kebab-case. The values the API reports today:

advance-payment    annual-listing     annual-return
deb                deb-arrivals       deb-dispatches
deb-des            esl                espl
extrastat          intrastat-arrival  intrastat-departure
ioss               it-esterometro     local-listing
oss                oss-non-union      vat-payment
vat-return

Treat this as an open set, not an enum to validate against: a return period whose type was never set reports unknown, and a new internal return type falls back to its kebab-cased name — so unrecognized values may appear and are valid. Unlike status, returnType is not a query filter.

Retries and idempotency

There is no idempotency-key header. Instead, the API's state changes are guarded by the 409 conflicts above, and that is what makes retrying safe in practice:

  • Reads (GET) are always safe to retry. They change nothing.
  • State-changing calls refuse to happen twice. If a POST times out on your side and you resend it, one of two things happens: the first attempt never landed and the retry simply succeeds, or the first attempt did land and the retry gets a 409 whose detail tells you so ("The period is already processing.", "This return period has already been submitted.", and so on). Treat that 409 as success-already-happened: GET the resource, confirm the state, move on.
  • One exception to watch: file uploads. Retrying an upload whose first attempt succeeded gets the duplicate-name 409. That is your signal the file is already there — do not upload it under a different name to "get past" the conflict, or the period will hold the same data twice.
  • Do not retry 400 or 422. Those mean the request itself is wrong; the same request gets the same rejection. Fix it first.

Polling etiquette

Today the API is poll-based: you start work with one call, then check on it with reads. (Webhooks are coming soon — see below.) Two things get polled in the flow:

1. The data period, after starting processing. POST .../processing returns 202 Accepted with a Location header — poll exactly that resource:

curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
  "$BASE/api/public/v1/companies/48213/data-periods/2026-06"

Keep polling while status is processing. Stop on either terminal state:

2. The return period, around approval and submission. GET .../return-periods/{returnPeriodId} reports the lifecycle: keep polling while the status is in-progress; waiting-for-approval means it is your turn to act, and approved, submitted, or error are states to react to, not to wait out. (An error on a return period is resolved by Taxually — you cannot act on it.)

How to poll well:

  • Always poll the resource the Location header gave you — do not construct polling URLs by hand.
  • Poll at a relaxed interval and back off if you have been waiting a while. Processing runs through to return generation in the background; a tight loop learns nothing sooner.
  • Stop as soon as you reach a terminal status. processing and in-progress are the only statuses that mean "ask again later".

Not available yet (coming soon)

The following are planned but not available today. Do not build against them; they are listed so you know the current shape is deliberate.

  • Webhooks. Today you poll (as above). Event notifications — for example "data period processed" or "return generated" — are coming soon.
  • "My transactions" API. An obligations-based view of your data, including payments. The contracts are being designed.
  • Fine-tuning beyond column mappings. The API currently lets you configure file setups and column mappings (see File setups & mapping). Configuring the rest of a company's fine-tune — tax code mappings, categorization rules, adjustments — via the API is a planned next step; today those are set up with your Taxually representative.

When any of these ship, they will be announced and documented; nothing on this page changes shape when they do.

Where to go next

For AI agents

This page is for AI agents working against the Taxually Public API, and for the humans wiring them up. It compresses the whole docs site into one read: what the API does, the exact call sequence that produces a tax return, an index of every endpoint, and the rules an agent must not break. If you load one page into context, load this one.

The Taxually Public API generates and files EU VAT returns from raw transaction data. You upload a month's transaction files for a company, start processing, poll until it completes, review the returns it produced, approve them, and download the artifacts. Version v1, 19 endpoints, JSON everywhere.

A machine-readable catalogue of all 19 endpoints — methods, paths, parameters, request bodies, response codes, enums, and conventions — is embedded in this page as <script type="application/json" id="api-index">. If you can parse JSON, start there; this section carries the process knowledge the catalogue cannot.

Ground truth in three lines

  • Base URL: the API is live on both environments — https://taxually-test-api.test.taxually.com on test, https://taxually-api.taxually.com in production. All paths below are relative to the base and start with /api/public/v1.
  • Auth: send Authorization: Bearer <key> on every request, or Authorization: Bearer <jwt>. Either scheme works alone; both credentials use the same header. API keys are created on the company settings page in the Taxually UI. How to obtain a JWT is not self-serve today — contact your Taxually representative. See Authentication.
  • Format: every response is application/json; every error is an RFC 7807 problem document (application/problem+json) with a human-readable detail you can surface or act on. See Errors & conventions.

Where the machine-readable spec lives

The OpenAPI 3.0.1 document is published by the API itself:

curl "$BASE/swagger/Public/swagger.json"

That path is the same on both environments: the spec document is at /swagger/Public/swagger.json on test and in production alike. The browsable Swagger UI lives at /swagger-public/index.html on both — https://taxually-test-api.test.taxually.com/swagger-public/index.html on test, https://taxually-api.taxually.com/swagger-public/index.html in production. The spec document and Swagger UI are both public — no credentials needed to read them. The spec is the canonical contract: every path, schema, enum, and security scheme in these docs traces back to it. If you can ingest OpenAPI, start there and use this page for the process knowledge the spec cannot carry — which call comes after which, and why.

The process, in one paragraph

Everything belongs to a company (set up once in the Taxually UI, not via the API — see Companies). Each company has data periods: months open for uploading transaction data. You upload one or more transaction files into a period, then start processing — a single call that runs all the way through to return generation; there is no separate finalization step. When the period completes, it has produced return periods: one per jurisdiction and return type (a June data period can yield a German VAT return, a Polish VAT return, and a German ESL). Each return waits for your approval, after which Taxually submits it — or, if you file it yourself, you upload the submission proof. Finally you download the documents each return produced.

Endpoint index

Nineteen endpoints, each tagged with the step it serves in the flow above. Steps marked – are supporting calls you make as needed.

StepMethodPathWhat it does
startGET/api/public/v1/companiesList the companies your credentials may act on. Each id is the companyId every other endpoint takes. Paged (limit 1–100, default 50; offset).
–GET/api/public/v1/companies/{companyId}Fetch one company; confirms an identifier is valid and accessible.
2GET/api/public/v1/mapping-propertiesList the transaction properties a file column can be mapped onto. Names are case-sensitive; consult before writing a file setup. Global — no company in the path.
2GET/api/public/v1/companies/{companyId}/file-setupsList the file setups the company currently uses to read its files, including Taxually-configured ones.
2POST/api/public/v1/companies/{companyId}/file-setupsCreate a file setup — teach the engine to read a custom file format. Only needed when no global mapping covers your format.
2PUT/api/public/v1/companies/{companyId}/file-setups/{setupName}Replace a file setup wholesale. Not a patch: omitted mappings are removed, omitted settings are cleared.
3GET/api/public/v1/companies/{companyId}/data-periodsList the months open for upload and their processing state. Newest first.
4POST/api/public/v1/companies/{companyId}/data-periods/{period}/transaction-filesUpload one transaction file (multipart, field file). Does not start processing. Returns the file's id — keep it.
–DELETE/api/public/v1/companies/{companyId}/data-periods/{period}/transaction-files/{fileId}Remove one file from the period so a corrected file can be uploaded under the same name. 204, no body. Does not reprocess — request processing afterwards.
–POST/api/public/v1/companies/{companyId}/data-periods/{period}/restartReset the period to its pre-upload state, discarding every uploaded file. Use only when the period's data is wrong as a whole; for one bad file, DELETE it instead.
5POST/api/public/v1/companies/{companyId}/data-periods/{period}/processingStart processing. 202, no body; Location header points at the status resource to poll. Runs through to return generation.
6GET/api/public/v1/companies/{companyId}/data-periods/{period}The polling target. Period status (new / processing / completed / error) plus per-file statistics: transactions read, errors, warnings.
7GET/api/public/v1/companies/{companyId}/return-periodsList the returns produced: jurisdiction, type, status, VAT amount, deadlines. Filters: dateFrom, dateTo (match period start), status. Not paged.
7GET/api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}One return period — the polling target between processing and waiting-for-approval, and after approval.
8POST/api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/approvalsApprove the return — your sign-off that it should be submitted. Some companies require two approvals from two different users.
altPOST/api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/submission-proofOnly when you file with the tax authority yourself: upload the proof (PDF or image, multipart, field file); marks the return submitted. Skip when Taxually files.
10GET/api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/documentsList everything the return produced: submission-file, draft, analytics, custom, proof-of-submission.
10GET/api/public/v1/companies/{companyId}/return-periods/{returnPeriodId}/documents/{documentId}/downloadShort-lived download link for one document. Follow it immediately; never store the URL.
10GET/api/public/v1/companies/{companyId}/data-periods/{period}/transaction-files/{fileId}/error-catalogueShort-lived link to a file's error catalogue (spreadsheet of errors/warnings found during processing). 404 means the file processed cleanly.

Full parameter tables, schemas, and per-endpoint samples: API reference. Field-level meaning in process context: Companies, File setups & mapping, Uploading data & processing, Returns, approval & submission, Documents & error catalogue.

Canonical recipe: generate a tax return

The one task this API exists for, as an executable plan. $BASE is the base URL; auth header on every call. Values shown are from the docs' running example (company 48213, period 2026-06). A prose walkthrough of the same flow is in Quickstart: generate your first tax return.

  1. Find the company.

    curl -H "Authorization: Bearer $TAXUALLY_API_KEY" "$BASE/api/public/v1/companies"

    Take the id of the company you are acting on — here 48213. Empty array means your credentials can act on no company: stop and escalate to a human.

  2. Check the upload window.

    curl -H "Authorization: Bearer $TAXUALLY_API_KEY" "$BASE/api/public/v1/companies/48213/data-periods"

    Pick a period with status new — here 2026-06. If the month you need is not listed, it is not open for upload (driven by VAT number validity and subscriptions); escalate rather than retry.

  3. (Conditional) Map custom file formats. Skip if your files come from a system with a global mapping (Amazon, Shopify, Stripe, SAP, Oracle, …). Otherwise: GET /api/public/v1/mapping-properties for the valid propertyName values (case-sensitive), then POST /api/public/v1/companies/48213/file-setups with the complete setup in one call. A 400 rejection lists every problem at once in its errors array — fix all of them, resubmit once. See File setups & mapping.

  4. Upload the files, one call each.

    curl -X POST -H "Authorization: Bearer $TAXUALLY_API_KEY" \
      -F "file=@amazon_de_2026-06.csv" \
      "$BASE/api/public/v1/companies/48213/data-periods/2026-06/transaction-files"

    Each 201 returns the file's id — record it; the error catalogue endpoint takes it. Uploading starts nothing.

  5. Start processing.

    curl -X POST -H "Authorization: Bearer $TAXUALLY_API_KEY" \
      "$BASE/api/public/v1/companies/48213/data-periods/2026-06/processing"

    Expect 202 Accepted with no body and a Location header (/api/public/v1/companies/48213/data-periods/2026-06). A 409 tells you exactly why it will not start — already processing, no files, or a failed file — in detail.

  6. Poll the data period (the Location URL) until status is completed or error. See the polling rules below.

    curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
      "$BASE/api/public/v1/companies/48213/data-periods/2026-06"

    On completed, check the per-file errorCount and warningCount. On error, read the error field and fetch the error catalogue for any file with errors (step 10). Recovery: DELETE …/transaction-files/{fileId} to remove the failed file, upload the corrected file, and start processing again. Producing the corrected file usually needs a human — surface the catalogue findings rather than retrying with the same data.

  7. Read the returns it produced.

    curl -H "Authorization: Bearer $TAXUALLY_API_KEY" \
      "$BASE/api/public/v1/companies/48213/return-periods?dateFrom=2026-06-01&dateTo=2026-06-30"

    Each entry carries status, amount, currency, isNil, submissionDeadline, paymentDeadline. Wait for waiting-for-approval (poll the single-return endpoint if needed). One data period can produce several returns — handle each.

  8. Approve — with a human in the loop. Approval is a legal sign-off that a tax return should be filed. Do not approve autonomously; surface the return's amount, currency, and deadlines to a human first.

    curl -X POST -H "Authorization: Bearer $TAXUALLY_API_KEY" \
      "$BASE/api/public/v1/companies/48213/return-periods/90417/approvals"

    201 returns approvedAt. If the company requires two approvals, the return stays waiting-for-approval until a different user approves; a repeat call by the same user is a 409, not a no-op.

  9. (Conditional) Record your own filing. Only when the partner submits to the tax authority instead of Taxually: POST …/return-periods/90417/submission-proof with the authority's proof document (PDF or image). The response is the return period, now submitted. When Taxually files, skip this step entirely.

  10. Download the artifacts. GET …/return-periods/90417/documents lists them; GET …/documents/{documentId}/download returns {name, url} where url is short-lived — download immediately, never persist the URL. Per uploaded file, GET …/transaction-files/{fileId}/error-catalogue works the same way (404 = the file was clean).

Polling rules

There are no webhooks today; webhooks are coming soon. Polling is the model, at two points in the flow:

  • After starting processing (step 6): poll GET …/data-periods/{period}. Terminal states are completed and error; keep polling on processing. Processing covers reading every file, tax calculation, and return generation, so expect it to take a while for large files — poll on the order of every 30–60 seconds with backoff, not in a tight loop. No rate limits are published, so be conservative.
  • While a return settles (step 7): poll GET …/return-periods/{returnPeriodId}. new and in-progress mean keep polling; waiting-for-approval means act (step 8); approved and submitted are progress you observe, not states you wait on a timer for; error means Taxually is resolving it — nothing for the caller to do.

Cap total polling time and escalate to a human on timeout rather than polling forever.

Constraints an agent must not violate

  • 404 hides existence. A company (or anything under it) you cannot access is reported as 404 Resource not found, identical to one that does not exist — never 403. Do not retry a 404 with different credentials expecting a 403 to distinguish the cases; treat 404 as "not yours, stop".
  • Statuses are closed sets; returnType is not. Data period statuses: new, processing, completed, error. Return period statuses: new, in-progress, waiting-for-approval, approved, submitted, error — these six are also the only accepted values of the status filter; a bad filter is a 400 whose detail lists all accepted values. Document types today: submission-file, draft, analytics, proof-of-submission, custom — handle unrecognized types tolerantly. returnType is an open set (see Return types): treat unrecognized values, including unknown, as valid — new types may appear without a version bump.
  • propertyName is case-sensitive. TransactionDate, not transactionDate. Always read GET /mapping-properties before writing a file setup; never guess names.
  • File setups are written whole, once. POST and PUT take the complete setup; PUT removes anything you leave out. Every write versions the company's whole mapping configuration, so batch all changes into one call — never build a setup up across several requests.
  • Download URLs expire. Every {name, url} response carries a short-lived link. Fetch it in the same breath you received it; never cache, log, or store the URL.
  • Nullable fields are omitted or null by design. error appears only when status is error; amount is null before calculation and for listings (like an ESL); submissionDeadline and paymentDeadline can be null. Build tolerant parsers.
  • 409 is a state message, not a transient failure. detail always says what is wrong and what to do ("The period is already processing.", "Upload transaction files first."). Read it and change course; do not blind-retry.
  • Approval and submission are consequential. They tell Taxually to file, or record, a legal tax return. Keep a human in the loop for steps 8 and 9.
  • Delete and restart are destructive; restart is period-wide. DELETE …/transaction-files/{fileId} removes one file; POST …/data-periods/{period}/restart discards every file in the period. Never reach for restart when one file is bad — delete that file. Neither call reprocesses anything: after replacing the data, request processing again. Both answer 409 while the period is processing — wait, do not retry in a loop.

llms.txt for this docs site

This is the llms.txt for this documentation site. Until it is served at /llms.txt, it is embedded here — both as the visible block below and, verbatim, in <script type="text/llms.txt" id="llms-txt"> for machine extraction. All documentation lives on this single page, so every link is a section anchor.

# Taxually Public API

> Partner-facing REST API for generating and filing EU VAT returns from raw
> transaction data: upload a month's transaction files for a company, start
> processing, poll to completion, approve the returns produced, download the
> artifacts. v1, 19 endpoints, JSON, auth via API key or JWT, both sent as
> Authorization: Bearer. Live on both environments — test:
> https://taxually-test-api.test.taxually.com, production:
> https://taxually-api.taxually.com. OpenAPI spec:
> /swagger/Public/swagger.json on both.

All documentation lives on one page; links are anchors into it.

## Docs

- [Overview](#overview): What the API does and the end-to-end flow from transaction files to filed return.
- [Authentication](#authentication): The two auth schemes, how to get an API key from the Taxually UI, what credentials scope you to.
- [Quickstart: generate your first tax return](#quickstart): The full happy path as copy-paste curl calls against the test environment.
- [Companies](#companies): Listing and reading the companies your credentials may act on; companyId is the key to everything else.
- [File setups & mapping](#file-setups): Teaching the engine to read custom transaction file formats; mapping properties; write-whole-setup rules.
- [Uploading data & processing](#upload-and-process): Data periods, uploading transaction files, deleting a file or restarting the period to correct data, starting processing, polling the period status.
- [Returns, approval & submission](#returns-and-approval): Return periods, their lifecycle statuses, approving, recording your own filing.
- [Documents & error catalogue](#documents): Listing and downloading return artifacts and per-file error catalogues via short-lived links.
- [Errors & conventions](#errors-and-conventions): RFC 7807 problem documents, the status-to-title table, return types, the file-setup errors array, 404-hides-existence.
- [For AI agents](#ai-agents): Endpoint index, canonical recipe, polling rules, hard constraints.

## Optional

- [API reference](#api-reference): Exhaustive per-endpoint parameter tables, schemas, and samples; the OpenAPI spec carries the same contract.

What is coming

Two things coming soon matter to agents. Webhooks: events such as "data period processed" and "return generated" are planned, replacing polling as the primary model — planned, not available; do not attempt to register a webhook today. Finer-grained operations: uploading against a single obligation and running individual steps (such as VAT-number validation) on their own are being designed. Until then, the flow on this page is the flow.