# Quarterly VAT filing (/quarterly-vat)

See how an app can help someone choose a taxpayer, prepare a quarterly VAT filing, check the amount due, and submit it with their permission.



The Quarterly VAT API lets an app help someone prepare and submit their quarterly
VAT filing. It supports **original filings in US dollars** for the person signed
in or a business they currently represent and have agreed to share with the app.

## What each VAT API does [#what-each-vat-api-does]

Each API handles one part of the filing process:

| API                                                                 | What it does for the user                                                                                                                                                                                                                                                    |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Choose the taxpayer and quarter](/reference/oauth-get-vat-context) | Shows the registered person or business name, their RPN, and open or overdue quarters to choose from. It also provides the declaration the person must accept before filing. The system checks that the chosen quarter is eligible when preparing and submitting the filing. |
| [Start or resume a draft](/reference/oauth-create-vat-draft)        | Starts a working copy for the chosen taxpayer and quarter, or brings back an existing draft with its saved figures. Starting a draft does not submit a return.                                                                                                               |
| [Save figures and calculate VAT](/reference/oauth-update-vat-draft) | Saves the reported income, tax credit, and tax already withheld, then calculates the amount due using the same calculation as the portal. Saving does not submit the filing or take a payment.                                                                               |
| [Check progress and confirmation](/reference/oauth-get-vat-filing)  | Shows the saved figures, amount due, and whether payment or a tax-credit review is needed. After submission, it shows the filing reference and submission date as confirmation.                                                                                              |
| [Submit the filing](/reference/oauth-submit-vat-draft)              | Files the reviewed figures after the person accepts the declaration and the required checks pass, including payment and any tax-credit approval. It returns confirmation of the submitted filing.                                                                            |

For example, someone can select their business and quarter, enter their figures,
check the calculated VAT, and confirm the declaration in a connected app. If no
tax is due and all checks pass, they can submit without making a payment. If tax
is due, the app directs them to the portal to pay, then checks for confirmation;
completing payment may also submit the filing.

## Identification and permission [#identification-and-permission]

The app uses the name and 14-digit RPN already registered with e-Próspera to show
whose tax return is being prepared. This does not perform a new identity check
or require another ID-document upload. Someone without a registered RPN must
finish registration in the portal first.

The person must sign in and explicitly allow the app to prepare and submit VAT.
For a business, they must also select that business and remain an active
representative. Knowing someone's RPN alone does not give access to their taxes.

## What still happens in the portal [#what-still-happens-in-the-portal]

* **Payments:** a filing with tax due uses the existing portal payment process.
  A filing with no tax due needs no invoice or payment.
* **Tax-credit approval:** a claim for a positive tax credit needs finance review
  through the portal before submission.
* **Other filing types:** Bitcoin filings, amendments to an existing return, and
  annual income-tax submissions remain portal workflows in this release.

Apps can also show outstanding filing periods, past returns, and downloadable tax
documents using the [tax information APIs](/tax-and-entity-data#what-each-tax-information-api-does).
Those information APIs require separate permission to read tax records.

The sections below explain how developers connect these steps to an app.

## Before you start [#before-you-start]

You will need:

1. An e-Próspera account with a registered RPN, and an active representative role
   for any business you want to file for.
2. An OAuth client enabled for VAT filing in the environment you are using.
   Request client setup or additional permissions through the
   [Help Center chat](https://help.eprospera.com/en/chat).
3. An access token issued after the person signs in and grants the required
   permissions. Standard Keys, Agent Keys, Partner Keys, and OIDC ID tokens do
   not authorize these VAT endpoints.
4. For the terminal walkthrough, `curl` with `--fail-with-body` support and `jq`.

Start in [staging](/testing-in-staging#oauth-in-staging) with an authorized test
account. Staging accounts and credentials are separate from production. Follow
that guide for OAuth discovery: the staging issuer may use a different hostname
from the API base URL.

The examples below use fictional names, IDs, dates, and figures. Replace them with
values from your own authorized responses. Run one step at a time and stop if a
request fails; `--fail-with-body` preserves the error response and returns a
nonzero exit code. Creating a draft saves a real record in the selected environment.

```bash
export BASE_URL='https://stg.portal.eprospera.com'
export ACCESS_TOKEN='<your-staging-oauth-access-token>'
```

Keep tokens and financial responses out of shared logs. The examples hold JSON
responses in shell variables rather than writing them to public files. Do not use
shell tracing (`set -x`) while handling credentials.

## Authorize the taxpayer [#authorize-the-taxpayer]

Use the [OAuth authorization-code flow](/oauth-overview#authorization-flow) with
PKCE. Request only the permissions your app needs:

| Taxpayer          | Required for all five VAT endpoints | Also needed to download assessments or read filing history |
| ----------------- | ----------------------------------- | ---------------------------------------------------------- |
| Signed-in person  | `eprospera:person.tax.vat.write`    | `eprospera:person.tax.read`                                |
| Selected business | `eprospera:entity.tax.vat.write`    | `eprospera:entity.tax.read`                                |

For example, an app that files VAT and downloads assessments for selected
businesses would request these scopes alongside its sign-in scopes:

```text
openid profile eprospera:entity.tax.vat.write eprospera:entity.tax.read
```

An administrator must enable the VAT scopes on the OAuth client's allowlist;
they are not default permissions. After enablement, obtain fresh user consent.
Existing read-only grants do not gain filing permission. The user selects which
businesses to share during consent, and current representation is checked on
every request. Revocation, account deactivation, or removal of the client's VAT
scope stops subsequent VAT access.

VAT context provides the identity needed to choose a taxpayer. If your integration
separately needs personal profile data or verified ID images, request the
corresponding [identity permissions](/oauth-overview#supported-scopes). An RPN does
not indicate that identity-verification images are available. No ID-document
upload is required by the VAT workflow.

### Endpoint map [#endpoint-map]

All endpoints return JSON wrapped in `data` and use an OAuth access token in the
`Authorization: Bearer` header. `{filingId}` is the ID returned when a draft is
created or resumed.

| Method | Path                                           | Purpose                                               |
| ------ | ---------------------------------------------- | ----------------------------------------------------- |
| GET    | `/api/v1/me/tax/vat/context`                   | Choose the taxpayer and quarter; get the declaration. |
| POST   | `/api/v1/me/tax/vat/filings`                   | Create or resume a draft.                             |
| PUT    | `/api/v1/me/tax/vat/filings/{filingId}`        | Save all figures and calculate VAT.                   |
| GET    | `/api/v1/me/tax/vat/filings/{filingId}`        | Read a draft or submission confirmation.              |
| POST   | `/api/v1/me/tax/vat/filings/{filingId}/submit` | Submit the reviewed filing.                           |

## Identify the taxpayer and quarter [#identify-the-taxpayer-and-quarter]

**Step 1:** get the taxpayers available to this app and the candidate quarters
for each one using [VAT context](/reference/oauth-get-vat-context):

```bash
VAT_CONTEXT="$(curl --fail-with-body -sS \
  "$BASE_URL/api/v1/me/tax/vat/context" \
  -H "Authorization: Bearer $ACCESS_TOKEN")"
printf '%s\n' "$VAT_CONTEXT" | jq .
```

Example response for an app authorized for one business:

```json
{
  "data": {
    "subjects": [
      {
        "subject": {
          "type": "legal_entity",
          "id": "11111111-1111-4111-8111-111111111111",
          "name": "Example Harbor LLC",
          "residentPermitNumber": "80123456789012"
        },
        "periods": [
          {
            "type": "vat",
            "period": { "year": 2026, "quarter": 3 },
            "dueDate": "2026-10-15T23:59:59.999Z",
            "status": "open",
            "filingId": null
          }
        ]
      }
    ],
    "supportedCurrencies": ["usd"],
    "declaration": {
      "version": "vat-v1",
      "text": "I am authorized to file for this taxpayer and confirm that I have reviewed these figures and that they are complete and accurate to the best of my knowledge."
    }
  }
}
```

Display the name, RPN, and quarter so the person can choose explicitly. Use the
returned due date; it can reflect a reviewed filing requirement. Quarters are
numbered 1–4. `filingId: null` means no draft ID was returned for that period.

| Value                   | How to use it                                                                                                |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| `subject=personal`      | Select the signed-in person's taxes. Do not send their person UUID instead.                                  |
| `subject=<entity-uuid>` | Select a business using its returned `subject.id`.                                                           |
| RPN                     | Display the registered taxpayer number. It is not the business UUID or an access credential.                 |
| `subjects: []`          | No taxpayers are accessible under the current grant. Check the requested permissions and business selection. |
| `periods: []`           | No candidate VAT quarters were returned for that taxpayer. Check the portal if a filing was expected.        |

You can narrow the context request with `?subject=personal` or
`?subject=<entity-uuid>`. Choose an actual taxpayer and period from your response:

```bash
VAT_SUBJECT='11111111-1111-4111-8111-111111111111'
TAX_YEAR=2026
TAX_QUARTER=3
```

For a personal filing, use `VAT_SUBJECT='personal'`. These sample values are not a
suggestion to file that quarter for every account. Eligibility is checked again
on create, save, and submission. A personal quarter can still be rejected when
it must be filed through a represented business. Missing RPNs must be resolved
through registration in the portal.

## Create or resume a draft [#create-or-resume-a-draft]

**Step 2:** [create or resume](/reference/oauth-create-vat-draft) the chosen
quarter. `jq` builds the JSON body without manually interpolating values:

```bash
VAT_DRAFT="$(jq -n \
  --arg subject "$VAT_SUBJECT" \
  --argjson year "$TAX_YEAR" \
  --argjson quarter "$TAX_QUARTER" \
  '{subject: $subject, year: $year, quarter: $quarter}' | \
  curl --fail-with-body -sS -X POST \
    "$BASE_URL/api/v1/me/tax/vat/filings" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H 'Content-Type: application/json' \
    --data-binary @-)"
printf '%s\n' "$VAT_DRAFT" | jq .

FILING_ID="$(printf '%s' "$VAT_DRAFT" | jq -er '.data.id')"
REVISION="$(printf '%s' "$VAT_DRAFT" | jq -er '.data.revision')"
```

A new draft returns **201**; resuming an existing draft returns **200**. Both
responses have the [same filing shape](#save-figures-and-get-the-calculation).
New drafts have `status: "draft"`, incomplete figures, `taxLiability: null`, and
`readyToSubmit: false`. Existing drafts retain their saved figures.

Keep the returned `id` and `revision`. A revision identifies the figures you last
read; send it back when saving or submitting so a stale screen cannot overwrite
someone else's changes. Treat it as an opaque value, not a timestamp or counter.

Repeating create for the same taxpayer, year, and quarter resumes the draft without
overwriting it. This combination provides retry protection; no separate
idempotency-key header is required. An already submitted quarter, multiple drafts,
or an existing BTC/amendment filing produces **409** and requires portal handling.

## Save figures and get the calculation [#save-figures-and-get-the-calculation]

**Step 3:** [save the complete figures](/reference/oauth-update-vat-draft). This
example uses a zero-income test quarter to demonstrate submission without payment.
Use the taxpayer's actual figures when building a real filing.

```bash
VAT_SAVED="$(jq -n --arg revision "$REVISION" '{
  revision: $revision,
  currency: "usd",
  accountingMethod: "cash",
  grossIncome: "0.00",
  taxCredit: "0.00",
  taxWithheld: "0.00"
}' | curl --fail-with-body -sS -X PUT \
  "$BASE_URL/api/v1/me/tax/vat/filings/$FILING_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @-)"
printf '%s\n' "$VAT_SAVED" | jq .
REVISION="$(printf '%s' "$VAT_SAVED" | jq -er '.data.revision')"
```

| Field              | What to send                                                             |
| ------------------ | ------------------------------------------------------------------------ |
| `revision`         | The latest revision returned by create, read, or save.                   |
| `currency`         | `"usd"` in this release.                                                 |
| `accountingMethod` | `"cash"` or `"accrual"`, matching the taxpayer's reporting method.       |
| `grossIncome`      | Reported gross income for the quarter, as a decimal string.              |
| `taxCredit`        | The claimed tax-credit amount. A positive claim requires finance review. |
| `taxWithheld`      | Tax already withheld, as a decimal string.                               |

Send **all three monetary fields**, including explicit zeros. Valid amounts
include `"0"`, `"1000.00"`, and `"12.5"`. JSON numbers (`1000`), negative amounts,
currency symbols, commas, and more than two decimal places are rejected. The
maximum is 12 integer digits and 2 decimal places. Unknown body fields are
rejected; do not supply taxpayer names, calculated liability, or payment status.

Example **200** response after saving the zero-income figures:

```json
{
  "data": {
    "id": "22222222-2222-4222-8222-222222222222",
    "subject": {
      "type": "legal_entity",
      "id": "11111111-1111-4111-8111-111111111111",
      "name": "Example Harbor LLC",
      "residentPermitNumber": "80123456789012"
    },
    "period": { "year": 2026, "quarter": 3 },
    "status": "draft",
    "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "currency": "usd",
    "accountingMethod": "cash",
    "reported": { "grossIncome": "0", "taxCredit": "0", "taxWithheld": "0" },
    "taxLiability": "0.00",
    "readyToSubmit": true,
    "creditReviewRequired": false,
    "creditReviewStatus": null,
    "paymentRequired": false,
    "invoice": null,
    "submittedAt": null,
    "links": {
      "portal": "/file/tax/22222222-2222-4222-8222-222222222222/confirm",
      "assessment": null
    }
  }
}
```

The sample revision is illustrative; always reuse your returned value. Monetary
responses are decimal strings and can omit trailing zeros. A `null` figure means
missing data, not zero. Display the server's `taxLiability` instead of calculating
it independently. Saving figures neither submits the filing nor takes payment.

Submitted filings and filings with processed invoices cannot be edited. Changing
figures voids an unprocessed invoice and invalidates any earlier tax-credit
approval. Read the returned state to determine the next action.

## Check progress and choose the next action [#check-progress-and-choose-the-next-action]

**Step 4:** [read the filing](/reference/oauth-get-vat-filing) after resuming work,
returning from the portal, or recovering from an uncertain response:

```bash
VAT_CURRENT="$(curl --fail-with-body -sS \
  "$BASE_URL/api/v1/me/tax/vat/filings/$FILING_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN")"
printf '%s\n' "$VAT_CURRENT" | jq .
REVISION="$(printf '%s' "$VAT_CURRENT" | jq -er '.data.revision')"
```

Read the flags together, starting with `status`. They are not separate filing
statuses: a draft can require both a credit review and payment.

| Returned state                                         | What your app should show or do                                                                         |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `status: "submitted"`                                  | Show confirmation. Stop saving or submitting; `readyToSubmit` is false after submission.                |
| `taxLiability: null`                                   | Ask for the missing figures or accounting method, then save.                                            |
| `creditReviewRequired: true`                           | Open the portal so the user can request finance review, then refresh after the decision.                |
| `paymentRequired: true`                                | Open the portal to prepare and complete payment, then read the filing again.                            |
| `readyToSubmit: true`                                  | Show the taxpayer, quarter, current figures, calculated tax, and declaration for explicit confirmation. |
| Draft with complete figures but `readyToSubmit: false` | Inspect the review/payment flags and portal state. Do not force submission.                             |

`readyToSubmit` covers figures, credit review, and payment readiness. Permissions,
period eligibility, and the declaration are still checked during submission.

For portal handoff, resolve `data.links.portal` against the same `BASE_URL` used
for the API, for example:

```text
https://stg.portal.eprospera.com/file/tax/22222222-2222-4222-8222-222222222222/confirm
```

Open the link in the user's browser; they may need to sign in. Do not append the
access token to it. `creditReviewStatus` reports the current finance review state;
use `creditReviewRequired` to determine whether approval still blocks submission.

### Common scenarios [#common-scenarios]

| Scenario                                    | Expected workflow                                                                                                                               |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Zero income, zero credit, zero withheld tax | Save figures, review the declaration, and submit when ready. No invoice or payment is needed.                                                   |
| Tax is due                                  | Save, open the portal to pay, and refresh. Payment completion may already have submitted the filing.                                            |
| A positive credit is claimed                | Request finance review through the portal even if the calculated amount due is zero. Refresh after approval and complete any remaining payment. |
| Figures changed in another session          | Read the latest filing and have the user review it again before saving or submitting.                                                           |
| The quarter was already filed               | Find it in [filing history](/tax-and-entity-data#list-submitted-filings). Use the portal if an amendment is needed.                             |

## Review, pay when needed, and submit [#review-pay-when-needed-and-submit]

**Step 5:** after any portal actions, read the latest filing as above. If it is
already submitted, use that confirmation. Otherwise, display its name/RPN,
quarter, accounting method, figures, and calculated liability alongside
`VAT_CONTEXT.data.declaration.text`.

Ask the person to confirm those exact figures and accept the declaration. Do not
pre-accept it during onboarding or reuse confirmation after figures change. Use
the revision they reviewed and the declaration version returned by context.

<Callout title="This next request submits the return" type="warn">
  Run it only after the person has reviewed the current filing and explicitly
  accepted the declaration. A successful save or a zero amount due is not
  consent to submit.
</Callout>

```bash
DECLARATION_VERSION="$(printf '%s' "$VAT_CONTEXT" | jq -er '.data.declaration.version')"

VAT_SUBMITTED="$(jq -n \
  --arg revision "$REVISION" \
  --arg version "$DECLARATION_VERSION" \
  '{revision: $revision, declaration: {version: $version, accepted: true}}' | \
  curl --fail-with-body -sS -X POST \
    "$BASE_URL/api/v1/me/tax/vat/filings/$FILING_ID/submit" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H 'Content-Type: application/json' \
    --data-binary @-)"
printf '%s\n' "$VAT_SUBMITTED" | jq '.data | {id, status, subject, period, taxLiability, submittedAt}'
```

Example output from that `jq` selection (the full response uses the same filing
shape as save/read):

```json
{
  "id": "22222222-2222-4222-8222-222222222222",
  "status": "submitted",
  "subject": {
    "type": "legal_entity",
    "id": "11111111-1111-4111-8111-111111111111",
    "name": "Example Harbor LLC",
    "residentPermitNumber": "80123456789012"
  },
  "period": { "year": 2026, "quarter": 3 },
  "taxLiability": "0",
  "submittedAt": "2026-10-04T18:00:00.000Z"
}
```

`status: "submitted"` plus the filing ID and `submittedAt` is the completion
signal. Keep the taxpayer, quarter, and figures with the confirmation so the user
can identify what was filed. The person, connected app, reviewed revision, and
accepted declaration version are also recorded in the tax audit trail.

Repeating a successful submission with the same revision returns the existing
confirmation. Do not create a new draft to recover from a lost response.

## Download the assessment [#download-the-assessment]

**Step 6:** a submitted filing includes `data.links.assessment`. Fetch that path
with the matching personal or business **tax read** permission; VAT write
permission alone does not authorize PDF downloads.

The following example uses the submitted response from step 5. If payment
submitted the filing in the portal, read it again and use that response's
assessment link instead.

```bash
ASSESSMENT_PATH="$(printf '%s' "$VAT_SUBMITTED" | jq -er '.data.links.assessment')"
umask 077
DOWNLOAD_DIR="$(mktemp -d)"
curl --fail -sS "$BASE_URL$ASSESSMENT_PATH" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -o "$DOWNLOAD_DIR/vat-assessment.pdf"
```

On success, the response is a PDF (`Content-Type: application/pdf`), not JSON.
The example saves it in a new private directory to avoid overwriting another
file. Check the command's exit status before opening or sharing it. VAT offers an
assessment PDF; the separate filed-return PDF is currently for annual income tax.
See [tax document downloads](/tax-and-entity-data#download-a-tax-document) for details.

<span id="errors-and-rollout" />

## Recover from errors and interrupted requests [#recover-from-errors-and-interrupted-requests]

Check the HTTP status before reading `data`. VAT errors contain `error` and may
also contain `message` or validation `details`. For example, a stale revision
returns **412**:

```json
{
  "error": "PRECONDITION_FAILED",
  "message": "Filing changed. Read it again and review the current figures."
}
```

| Status or situation                   | How to recover                                                                                                                                                                                                  |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` invalid request                 | Check JSON, required fields, decimal strings, and the declaration version. Read `details` when present. Resolve missing figures or unpaid tax before submission.                                                |
| `401` unauthorized                    | Refresh an expired access token if your grant supports it. If access was revoked or refresh fails, reconnect through OAuth.                                                                                     |
| `403` insufficient permission         | Check the requested personal/business scope, the client's enabled scopes, and fresh user consent. Repeating the same request cannot add permission.                                                             |
| `404` taxpayer or filing unavailable  | Verify the returned ID, selected business, current representation, and environment. It may be missing or outside the user's authorization.                                                                      |
| `409` conflict                        | Read the message and refresh context/filing. Missing RPNs, duplicate drafts, completed quarters, BTC filings, and amendments need portal handling; a concurrent change needs a fresh read and review.           |
| `412` changed figures or approval     | Read the current filing, show changed figures, and obtain fresh review before retrying with its revision. If credit approval changed, resolve the review through the portal. Do not silently overwrite figures. |
| `429` rate limit                      | Wait the number of seconds in `Retry-After` before retrying. The VAT limit is 60 requests per minute per OAuth client and user.                                                                                 |
| Timeout, connection failure, or `500` | The write may have completed. Recover using the operation-specific rules below instead of assuming nothing changed.                                                                                             |

| Interrupted operation | Recovery rule                                                                                                                                                                                                     |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Create                | Repeat with the same taxpayer, year, and quarter. This resumes an existing draft without replacing its figures; handle any returned conflict.                                                                     |
| Save                  | Read the filing first and compare all returned figures with the intended changes. If they were saved, continue from the returned revision. Otherwise, have the user review the current state before saving again. |
| Submit                | Read the same filing ID. If submitted, show its confirmation. If still a draft, recheck requirements and retry only with the current, reviewed figures and declaration.                                           |
| Portal payment        | Read the filing before another payment or submission attempt. Portal payment may already have submitted it.                                                                                                       |

Responses are private and non-cacheable. Scope any application storage to the
connected user and business, and remove access when the connection is revoked.

## Check your integration in staging [#check-your-integration-in-staging]

Use an authorized test account and quarters that its context actually returns:

* Confirm the taxpayer name/RPN and selected quarter before every submission.
* Create twice for the same taxpayer/quarter and confirm the same draft is resumed.
* Save zero-income figures and verify submission works without payment.
* Save a filing with tax due, complete portal payment, and read its final status.
* Test a positive credit claim through finance review; changing the figures must
  require review again.
* Save once, then attempt another save with an older revision and handle `412`.
* Recover a lost submission response by reading the same filing ID.
* Confirm another user's filing, an unselected business, and a removed
  representative cannot access or modify the filing.
* Revoke the app and verify it requires reconnection.
* Download an assessment with tax read permission and handle its absence gracefully
  when only VAT filing permission was granted.

Use the [staging guide](/testing-in-staging) for account setup and portal payment
testing. BTC filings, amendments, annual income-tax writes, and direct API payment
initiation remain portal workflows in this release.
