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
| Environment | Base URL |
| Test | https://taxually-test-api.test.taxually.com |
| Production | https://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:
- 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.
- 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.
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
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
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
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.
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.
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.
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
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.
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.
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 — the flow above as a runnable sequence of curl calls against the test environment.
- Authentication — the two ways to authenticate (an API key or a JWT, both sent as
Authorization: Bearer), and how to get a key.
- Companies, File setups & mapping, Uploading data & processing, Returns, approval & submission, Documents & error catalogue — each step of the process in depth.
- API reference — every endpoint, parameter and schema.
- Errors & conventions — the problem-document format, status codes, and paging.
- For AI agents — the machine-readable summary of everything above.
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:
- Check your companies — find the
companyId everything else needs
- (Optional) Teach the engine to read your files — only for custom file formats
- Find the open data period — which month is accepting data
- Upload your transaction files — one call per file
- Start processing — one call kicks off everything through return generation
- Poll until processing completes — watch the status, read the per-file statistics
- Inspect the returns — the returns the month produced, with amounts and deadlines
- Approve a return — your sign-off that it should be submitted
- List the return's documents — the return file, draft, and analytics
- 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:
| Status | Meaning |
new | Ready to receive data; nothing processed yet. |
processing | Being worked on — keep polling. |
completed | Calculated; its returns are available. |
error | Processing 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 — both credential schemes, and how keys map to companies.
- File setups & mapping — column mappings in depth: detection flags, replacing setups, and the full validation error format.
- Uploading data & processing — the data-period lifecycle, recovering from processing errors, and polling guidance.
- Returns, approval & submission — the full return lifecycle, two-person approval, and recording filings you make yourself.
- Documents & error catalogue — every document type and how download links behave.
- For AI agents — a condensed, machine-oriented version of this entire flow.
- API reference — every endpoint, parameter and status code.
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:
- 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.
- 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.
- 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.
| Scheme | Header | Value format |
| API key | Authorization | Bearer <api-key> — the API key value, prefixed with the word Bearer |
| Bearer | Authorization | Bearer <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
| Environment | Base URL |
| Test | https://taxually-test-api.test.taxually.com |
| Production | https://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:
| Endpoint | What it does |
GET /api/public/v1/companies | Lists 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.
| Field | Type | Meaning |
id | integer | The identifier of the company. This is the value to send as companyId on every other endpoint. |
name | string | The registered legal name of the company. |
countryCode | string, nullable | Two-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
| Name | In | Type | Meaning |
limit | query | integer | Optional. Maximum number of companies to return; between 1 and 100. Defaults to 50. |
offset | query | integer | Optional. 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
| Name | In | Type | Meaning |
companyId | path | integer, required | The 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:
- How to parse the file — its date format, decimal and thousand separators, and optionally a filename pattern.
- 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:
type | The column must contain |
date | Dates |
decimal | Decimal numbers |
integer | Whole numbers |
boolean | True/false values |
text | Free 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.
| Field | Type | Meaning |
name | string | The name to send as propertyName in a column mapping. Case-sensitive. |
type | string | What 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
| Field | Type | Meaning |
name | string | The name of the file setup, unique within the company. It is the identifier the setup is addressed by, matched case-insensitively. |
dateFormat | string, nullable | The .NET date format the date columns are parsed with, e.g. yyyy-MM-dd. Absent when the setup does not set one. |
decimalSeparator | string, nullable | The single character separating decimals in numeric columns, e.g. .. Absent when not set. |
thousandSeparator | string, nullable | The single character grouping thousands, e.g. ,. Absent when not set. |
fileNamePattern | string, nullable | A 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. |
columnMappings | array | Which column of the file carries which transaction property. |
Column mapping fields
| Field | Type | Meaning |
fieldName | string | The column header as it appears in your file, e.g. invoice_date. Matched case-insensitively, unique within the setup. |
propertyName | string | The transaction property the column is mapped onto, e.g. TransactionDate. One of the names from Step 1. |
required | boolean | Whether a row that leaves this column empty is rejected. Only applies to files that carry the column at all. |
requiredForVatDetection | boolean | Whether this column has to be present for the file to be recognized as carrying VAT transactions. |
requiredForIntrastatDetection | boolean | Whether this column has to be present for the file to be recognized as carrying Intrastat data. |
requiredForGlDetection | boolean | Whether 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)
| Field | Type | Meaning |
name | string, required | The name of the file setup, unique within the company, at most 30 characters. |
dateFormat | string, 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. |
decimalSeparator | string, optional | The single character separating decimals, e.g. .. |
thousandSeparator | string, optional | The single character grouping thousands, e.g. ,. |
fileNamePattern | string, optional | Regex matched against uploaded file names, at most 50 characters. Set it when several setups would otherwise match the same columns. |
columnMappings | array, required | At 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
| Status | When |
400 | The setup was rejected; nothing was stored. The problem document's errors member lists every reason at once — see Validation errors below. |
404 | Company missing or inaccessible. |
409 | The 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:
GET the setup (Step 2) to see what is stored.
- Change what needs changing.
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
| Status | When |
400 | Rejected; the errors member lists every reason. Also returned when the body's name differs from the name in the URL. |
404 | The company has no setup with this name, or the company is inaccessible. |
409 | The 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
}
]
}
| Field | Meaning |
errors[].message | What is wrong, in words you can act on. |
errors[].columnMappingIndex | The 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:
GET /data-periods — find which months are open for upload.
POST /data-periods/{period}/transaction-files — upload the month's files, one call per file.
POST /data-periods/{period}/processing — start processing. One call, no further steps.
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" }
]
| Field | Type | Meaning |
period | string | The data period, in yyyy-MM format. |
status | string | Processing 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"
}
| Field | Type | Meaning |
id | string (uuid) | The document identifier of the uploaded file. Keep it — the error-catalogue endpoint on Documents & error catalogue takes it. |
name | string | The 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:
| Status | Why |
400 | The period isn't yyyy-MM, the request isn't multipart, or the file is empty. |
404 | No data period for that company and month, or the company is inaccessible. |
409 | The period is currently being processed — wait for it to finish. |
409 | A 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:
| Status | Why |
400 | The period isn't yyyy-MM. |
404 | No data period for that company and month, no file with that fileId in it, or the company is inaccessible. |
409 | The 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:
| Status | Why |
400 | The period isn't yyyy-MM. |
404 | No data period for that company and month, or the company is inaccessible. |
409 | The 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:
| Status | Why |
400 | The period isn't yyyy-MM. |
404 | No data period for that company and month, or the company is inaccessible. |
409 | Already processing: "The period is already processing." |
409 | Nothing to process: "There are no files to process in 2026-06. Upload transaction files first." |
409 | A 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:
| Status | Meaning | What you do |
new | The period is ready to receive data; nothing has been processed yet. | Upload files, start processing. |
processing | The period is being worked on. | Keep polling. |
completed | The period has been calculated and its returns are available. | Move on to Returns, approval & submission. |
error | Processing 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
}
]
}
| Field | Type | Meaning |
period | string | The data period, in yyyy-MM format. |
status | string | new, processing, completed or error. |
error | string, nullable | A short, human-readable reason for the failure. Only present when status is error; omitted otherwise. |
files | array | The transaction files processed for this period. Empty when the period has none. |
Each entry in files:
| Field | Type | Meaning |
id | string (uuid) | The document identifier of the uploaded file — the same id the upload receipt returned, and what the error-catalogue endpoint takes. |
name | string | The original name of the uploaded file. |
transactionCount | integer | The number of transactions read from the file. |
errorCount | integer | The number of processing errors found in the file. |
warningCount | integer | The 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:
| Parameter | Type | Meaning |
dateFrom | yyyy-MM-dd | Only returns whose period starts on or after this date. |
dateTo | yyyy-MM-dd | Only returns whose period starts on or before this date. |
status | string | Only 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
| Field | Meaning |
id | The return period identifier. Every endpoint on this page takes it. |
country | The jurisdiction the return is filed in. |
returnType | The 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 / periodEnd | The first and last day of the period the return covers. |
status | Where the return is in its lifecycle — see below. |
error | Why producing the return failed. Only present when status is error and the reason is one you can be told. |
amount | The 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. |
currency | The currency the return is reported in. Null when it cannot be resolved. |
isNil | Whether the return reports nothing for the period. Null while not calculated. |
submissionDeadline | The 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. |
paymentDeadline | The 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:
| Status | Meaning | What you do |
new | The return period exists, but no return has been calculated for it yet. | Nothing — upload data and start processing if you haven't. |
in-progress | The return is being worked on. | Keep polling. |
waiting-for-approval | The return has been calculated and is waiting for your sign-off. Also reported while a required second approval is outstanding. | Review it, then approve. |
approved | You have approved it; it is waiting to be submitted. | Nothing — unless you file it yourself (see below). |
submitted | The return has been submitted to the tax authority. | Done. Download the proof from Documents & error catalogue. |
error | Producing 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
| Status | When | What to do |
400 | No 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. |
404 | No 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 state | The 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 files | You file |
| 1 | Approve: POST …/approvals | Approve: POST …/approvals |
| 2 | Poll until submitted | Download the submission-file document |
| 3 | Download the proof from the document list | Submit 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.
Download links are short-lived
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>"
}
| Field | Type | Meaning |
name | string | The name of the document, including its extension. |
url | string | The 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"
}
]
| Field | Type | Meaning |
id | string (uuid) | The identifier of the document, as accepted by the download endpoint of the same return period. |
name | string | The name of the document, including its extension. |
type | string | What 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:
| Value | Meaning |
submission-file | The return file itself, in the format the tax authority accepts. |
draft | The readable draft of the return, 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. |
custom | An 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.
| Status | Why |
404 | No 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.
| Status | Why |
400 | The period isn't yyyy-MM. |
404 | No 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