# anybill Vendor API Integration (POS)

This project integrates the anybill Vendor API to issue digital receipts from
a point-of-sale (POS) system. This file provides context for AI coding agents
working on the integration.

## What is anybill

anybill is a digital receipt platform for retail merchants. The POS system
sends every receipt to the Vendor API; anybill renders it, hands it to the
customer via QR code or a known customer identifier, and forwards it to the
merchant's apps, loyalty systems and webhooks.

## Official documentation

- Vendor API index (machine-readable): https://developer.anybill.de/downloads/anybill-vendorapi-llms.txt
- Getting started, retry policy, Postman collection: https://developer.anybill.de/vendor_api/index.md
- Environments: https://developer.anybill.de/vendor_api/environments.md
- Authentication: https://developer.anybill.de/vendor_api/authentication.md
- Bill endpoints: https://developer.anybill.de/vendor_api/bill_endpoints.md
- Bill data model: https://developer.anybill.de/vendor_api/bill_data_model.md
- Fiscalization (security object, mandatory fields per type): https://developer.anybill.de/vendor_api/fiscalization.md
- Bill examples (discounts, returns, custom sections): https://developer.anybill.de/vendor_api/bill_examples.md
- Print buffer format: https://developer.anybill.de/vendor_api/print_buffer.md
- Store endpoints: https://developer.anybill.de/vendor_api/store_endpoints.md
- Loyalty endpoints: https://developer.anybill.de/vendor_api/loyalty_endpoints.md
- Customer and cashier display: https://developer.anybill.de/vendor_api/customer_display.md
- Integration review checklist: https://developer.anybill.de/vendor_api/integration_review.md
- C# POS SDK (preferred for .NET): https://developer.anybill.de/vendor_api_sdk/overview.md
- OpenAPI specification (production): https://vendor.anybill.de/api/swagger/v3/swagger.json
- OpenAPI specification (staging): https://vendor.stg.anybill.de/api/swagger/v3/swagger.json

When in doubt, fetch the relevant `.md` page or the OpenAPI specification and
follow the official contract. Do not invent field names.

## Environments

- Staging (integration testing): `https://vendor.stg.anybill.de/api`
- Production: `https://vendor.anybill.de/api`

All routes below are relative to this base URL, i.e. `POST /v3/bill` is
`https://vendor.stg.anybill.de/api/v3/bill`. Do not drop the `/api` prefix.

Credentials (client id, service account) differ per environment. Base URL and
credentials must be configurable per merchant installation; switching a till
from staging to production must not require a new POS release.

## Authentication model

The Vendor API uses OAuth 2.0 with the resource-owner password flow against
Azure AD B2C:

- Token endpoint:
  `https://adanybill.b2clogin.com/ad.anybill.de/oauth2/v2.0/token?p=b2c_1_ropc_vendor`
- Content type: `application/x-www-form-urlencoded`
- `client_id`: assigned once per POS manufacturer
- `username` / `password`: service account, assigned per merchant
- `scope`: `https://ad.anybill.de/vendor/bill offline_access` (add
  `https://ad.anybill.de/vendor/store`, `.../vendor/customer` or
  `.../vendor/user` when those endpoint groups are used)
- Every API call: `Authorization: Bearer <access_token>`

Token life cycle rules:

- Cache the access token. Never request a new token per receipt.
- Renew with `grant_type=refresh_token` before `expires_in` runs out. Tills
  are often open for more than 24 hours; the renewal must work mid-shift and
  without any cashier interaction.
- If refresh fails, fall back to a full password grant once, then treat the
  failure like an unreachable API (queue the receipt, see below).

## Receipt flow contract

Endpoints, all relative to the environment base URL:

- `POST /v3/bill/url` – pre-register a receipt id and get the receipt URL
  before the receipt exists. Use it to show the QR code before payment and to
  make the later `POST /v3/bill` idempotent.
- `POST /v3/bill` – send the receipt (`AddBillDto` with `storeId` and `bill`).
- `DELETE /v3/bill/{billId}` – cancel a receipt (204 once the cancellation
  is accepted; it is processed asynchronously, an unknown id is not reported).
- `POST /v3/bill/{billId}/printed` – record that a paper copy was printed
  after the digital receipt was issued.

Response handling for `POST /v3/bill`:

- `type: "url"` – anonymous receipt. Render `url` into the QR code
  **unchanged**; do not rewrite, shorten or re-encode it.
- `type: "externalId"` with `isAssigned: true` – the receipt was assigned to
  a known customer. Show a confirmation instead of a QR code.
- A request with `userIdentification.externalId` that cannot be matched
  returns `type: "url"`, not an error. Handle both types on every call.

Create a digital receipt for **every** transaction automatically. Issuing the
digital receipt must not require a manual step by the cashier.

## Resilience and error handling

- Classify every status code of `POST /v3/bill` explicitly:
  - `200` – stored; read `billId` and `url`/`type` from the body.
  - `400` – validation error, problem details with `errors` and `traceId` in
    the body. Never retry; log the body and fix the payload.
  - `401` – token missing, expired or invalid. Do not back off: refresh the
    token once and resend the same request. A second 401 is a credentials
    problem – stop and alert.
  - `403` – missing scope or permission. Never retry; configuration issue.
  - `404` – unknown `storeId`, or `bill.id` was set without a prior
    `POST /v3/bill/url` (`pregeneratedbill-notfound`). Never retry.
  - `409` – a receipt with this `bill.id` already exists. Never retry; treat
    it as delivered.
  - `504` – the receipt could not be stored in time (storage temporarily
    saturated); it was **not** stored. No body, no `Retry-After` header.
    Retry with backoff, same `bill.id`.
  - `500`, `502`, `503` and network errors/timeouts – retry with backoff,
    same `bill.id`. A 409 on the retry means the first attempt succeeded.
  - The API does not return `429` for `POST /v3/bill`; do not build
    client-side throttling around it.
- Exponential backoff with jitter: base delay 1 s, `1s * 2^attempt * random(0.5, 1.5)`,
  capped at 30 s, 3 to 5 attempts. Only 5xx and network errors count as
  attempts.
- Retries must not create duplicate receipts. Register the id via
  `POST /v3/bill/url` first and send the receipt with that id, so a retry
  reuses the same id.
- Persist receipts that could not be delivered in a queue that survives a
  restart of the till, and drain it in the background. Define an upper bound
  for how long receipts stay queued.
- Do not block the sale on the anybill call. Call the API asynchronously or
  with a short timeout; if the digital receipt is unavailable, show the
  error case on the displays and offer a paper receipt.
- Log every failed request including the `traceId` from the response body.
  Without it anybill cannot trace the request.

## Receipt content rules

These rules go beyond the OpenAPI schema and are checked during the
integration review. Apply them when mapping the till's receipt model to
`AddBillDto`.

Totals and taxes:

- `bill.data.fullAmountInclVat` equals the sum of all lines and the sum of
  all `paymentTypes[].amount`.
- `vatAmounts` are unique by `percentage`, both on receipt level and on
  every line. Send tax on line level, including 0 % lines.
- Every `vatAmount` referenced by a discount in the anybill extension has a
  matching `vatAmount` on the default line.

Lines and items:

- Send the item identification the till has: `text` (name), `item.number`
  (internal article number), GTIN/EAN, product group, PLU where available.
- Send `item.quantity` with the correct `quantityMeasure` (weight, volume,
  count) and `item.pricePerUnit`.
- Returns are a **new** receipt with negative amounts whose returned line
  references the original receipt via
  `extension:anybill.lineReturnReference` (`returnCodeReference` = the
  original receipt's `returnBarcode`, optionally `originalReceiptIdReference`
  = its `billId`). `returnBarcodeReference` and `isReturn` are deprecated.
  Cancelled lines are still transmitted.

Discounts:

- A discount on a single item stays on that line (anybill extension).
- A discount on the whole receipt is a separate `DiscountLineDto` with
  negative values, never a reduced item price.
- Every discount has a unique `id` and is listed in the receipt-level
  discount list of the anybill extension.

Payments:

- Map every payment type the till produces. Use `CardPayment` for card
  payments; `CreditCard`, `Girocard`, `Maestro` and `VisaElectron` are
  deprecated. Support split payments (several `paymentTypes`).
- Cash payments include the change given.
- For card payments send the terminal customer receipt in
  `bill.misc.additionalReceipts` and fill `CardPaymentDetails` (terminal id,
  date/time, trace number, auth code). The card PAN must be **masked**.
- `foreignAmount` and `foreignCurrency` are always sent together; currency
  codes are ISO 4217.

Head and cash register:

- Send the serial number of the cash register and the receipt number of the
  till in `bill.head`. The receipt number is what the merchant reconciles
  with and is forwarded as `receiptNr` in the receipt notification webhook.
- `bill.head.date` is the transaction date and time with time zone offset.

Fiscalisation (`bill.security`):

- Germany (TSE): send the complete TSE data – serial number, start and end
  timestamp, transaction number, signature counter, signature and the QR
  code data.
- Austria (RKSV): send the signature data and the QR code for the
  Kassennachschau, plus the cash register id.
- France (BOI-TVA): send `cashRegister.version`, the `Siret` and `NAF`
  country-specific attributes and the receipt transaction type
  (purchase or return).
- Spain (TicketBAI): send `qrCodeData`, `link`, `id` and `number`.
- The fiscalisation data is sent in `security.fiscalization`; its `type`
  selects the country-specific structure (`Tse`, `Rksv`, `BoiTva`, `TBai`,
  ...). Without `type` the object is treated as `Tse`.
- Countries without fiscalisation (e.g. Switzerland, Netherlands, Belgium,
  Luxembourg): set `required` to `false` in the security extension.
- If the fiscal unit failed, still send the receipt and set `failure` to
  `true` in the security extension for that transaction; only then may the
  `fiscalization` object be omitted. Optionally send only `type` and
  `additionalLegalText` to print the outage details; anybill adds the
  failure notice for German and Austrian stores. Return to normal
  receipts as soon as the unit is back; do not resend failed ones.
- `security.tse`, `tseFailure` and `tseRequired` are deprecated aliases; do
  not use them in new code.
- The mandatory fields per `type` and complete examples:
  https://developer.anybill.de/vendor_api/fiscalization.md

Receipt types and modules:

- Receipts relevant for e-invoicing carry the e-invoice receipt type in the
  anybill misc extension.
- Hospitality receipts (Bewirtungsbeleg): include the tip when it is known;
  for amounts of 250 EUR and above the invoice recipient is legally required.
- If the merchant receipt (Händlerbeleg) module is used, send the merchant
  copy of the terminal receipt as `VendorReceipt` in `additionalReceipts`
  for card payments, and print and queue it when it cannot be transmitted.

## Customer assignment

- Use `userIdentification.externalId` for a known customer (loyalty card
  number, customer number). `userIdentification.userId` and
  `userIdentification.loyaltyCardBarcode` are deprecated.
- Transparent user creation for unknown `externalId` values has to be
  enabled per merchant by anybill. Do not assume it is active.
- Loyalty transactions (`PUT /v3/loyalty/transactions`) are started before
  the receipt is sent and linked to it; define what the till does if the
  loyalty provider does not answer in time.

## Store master data

- Every receipt carries the `storeId` of the anybill store. Stores are
  created once (portal or `POST /v3/store`) and their ids must stay stable
  when a store is renamed or moves.
- When several tills in one store can sync independently, call
  `POST /v3/store/search` by address first so that two tills do not create
  two stores.
- Always set an external store identifier that matches the merchant's own
  store list.

## Customer and cashier display

Implement the four display cases from the customer display page:

1. Anonymous customer, QR code after payment.
2. Anonymous customer, QR code before payment (requires `POST /v3/bill/url`;
   the receipt page updates once the receipt is sent).
3. Known customer: confirmation "receipt was sent" instead of a QR code.
4. Error / digital receipt unavailable: clear message and paper fallback.

The QR code must be readable on the customer display, with a call-to-action
text (for example "Scan now for your digital receipt"), and the cashier
display must prompt the cashier to inform the customer. If a paper copy is
printed later, call `POST /v3/bill/{billId}/printed` and mark the print as a
copy. Without a customer display, print the QR code on the paper receipt.

## Reference snippets

The snippets are starting points for tills built on older platforms; adapt
them to the host code base. For .NET, prefer the official C# POS SDK
(https://developer.anybill.de/vendor_api_sdk/csharp_integration.md), which
implements authentication, token caching and the data model.

### curl – obtain a token and send a receipt

```bash
curl -s -X POST \
  'https://adanybill.b2clogin.com/ad.anybill.de/oauth2/v2.0/token?p=b2c_1_ropc_vendor' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=password' \
  -d 'client_id=<client id of the POS manufacturer>' \
  -d 'username=<service account username>' \
  -d 'password=<service account password>' \
  -d 'scope=https://ad.anybill.de/vendor/bill offline_access' \
  -d 'response_type=token'

curl -s -X POST 'https://vendor.stg.anybill.de/api/v3/bill' \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d @receipt.json
```

### C# (.NET Framework 4.7.2, HttpClient) – token cache

```csharp
public sealed class AnybillTokenProvider
{
    private readonly HttpClient _http;
    private readonly string _clientId, _username, _password;
    private string _accessToken, _refreshToken;
    private DateTimeOffset _expiresAt = DateTimeOffset.MinValue;
    private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);

    private const string TokenUrl =
        "https://adanybill.b2clogin.com/ad.anybill.de/oauth2/v2.0/token?p=b2c_1_ropc_vendor";
    private const string Scope = "https://ad.anybill.de/vendor/bill offline_access";

    public async Task<string> GetAccessTokenAsync()
    {
        await _lock.WaitAsync();
        try
        {
            if (_accessToken != null && DateTimeOffset.UtcNow < _expiresAt.AddMinutes(-5))
                return _accessToken;

            var form = _refreshToken != null
                ? new Dictionary<string, string>
                  {
                      ["grant_type"] = "refresh_token", ["refresh_token"] = _refreshToken,
                      ["client_id"] = _clientId, ["scope"] = Scope, ["response_type"] = "token",
                  }
                : new Dictionary<string, string>
                  {
                      ["grant_type"] = "password", ["username"] = _username, ["password"] = _password,
                      ["client_id"] = _clientId, ["scope"] = Scope, ["response_type"] = "token",
                  };

            var response = await _http.PostAsync(TokenUrl, new FormUrlEncodedContent(form));
            if (!response.IsSuccessStatusCode && _refreshToken != null)
            {
                _refreshToken = null;          // refresh failed: one full login, then give up
                return await GetAccessTokenAsync();
            }
            response.EnsureSuccessStatusCode();

            var json = JObject.Parse(await response.Content.ReadAsStringAsync());
            _accessToken = (string)json["access_token"];
            _refreshToken = (string)json["refresh_token"] ?? _refreshToken;
            _expiresAt = DateTimeOffset.UtcNow.AddSeconds((int)json["expires_in"]);
            return _accessToken;
        }
        finally { _lock.Release(); }
    }
}
```

### Java 8 (HttpURLConnection) – send a receipt with retry

```java
// baseUrl = "https://vendor.stg.anybill.de/api" (configurable per installation)
int[] delaysMs = {1000, 2000, 4000, 8000, 16000};
boolean tokenRefreshed = false;
for (int attempt = 0; attempt < delaysMs.length; attempt++) {
    HttpURLConnection con = (HttpURLConnection) new URL(baseUrl + "/v3/bill").openConnection();
    con.setRequestMethod("POST");
    con.setRequestProperty("Authorization", "Bearer " + tokenProvider.getAccessToken());
    con.setRequestProperty("Content-Type", "application/json");
    con.setConnectTimeout(5000);
    con.setReadTimeout(10000);
    con.setDoOutput(true);
    int status;
    try {
        try (OutputStream os = con.getOutputStream()) { os.write(receiptJson.getBytes("UTF-8")); }
        status = con.getResponseCode();
    } catch (IOException networkError) {                                   // timeout / connection lost
        status = -1;                                                       // treat like 5xx: retry, same bill.id
    }

    if (status >= 200 && status < 300) { return readBody(con); }          // stored
    if (status == 409) { return null; }                                    // already stored by an earlier attempt
    if (status == 401 && !tokenRefreshed) {                                // token expired: refresh once, resend
        tokenProvider.refresh(); tokenRefreshed = true; continue;
    }
    if (status >= 400 && status < 500) {                                   // 400/401/403/404: do not retry
        throw new AnybillClientError(status, readError(con));
    }
                                                                           // 5xx (incl. 504), network error: backoff
    long jitter = (long) (delaysMs[attempt] * (0.5 + Math.random()));
    Thread.sleep(Math.min(jitter, 30000));
}
queue.enqueue(receiptJson);                                               // give up for now, deliver later
```

## Common pitfalls

- Requesting a token for every receipt, or never refreshing it, so the till
  fails after 24 hours.
- Hardcoding the staging base URL or credentials in the POS binary.
- Retrying 400/403/404/409, backing off on 401 instead of refreshing the
  token, treating 504 as a permanent error, or retrying 5xx without a
  pre-registered receipt id (duplicates).
- Blocking the checkout on the API call instead of queueing.
- Shortening or re-encoding the receipt `url` before rendering the QR code.
- Sending a whole-receipt discount as reduced item prices instead of a
  discount line, or discounts without unique ids.
- Sending individual card brands (`CreditCard`, `Girocard`, ...) instead of
  `CardPayment`, or an unmasked PAN.
- Omitting the `fiscalization` object without `failure: true` or
  `required: false` in the security extension.
- Using the deprecated `userId` / `loyaltyCardBarcode` fields instead of
  `externalId`.
- Creating duplicate stores because tills cannot see each other; search by
  address first.
- Leaving footer texts, legal notices, coupons or loyalty numbers off the
  digital receipt that are on the paper receipt.

## When generating code

- Match the language, framework version and conventions of the existing
  POS code base (for example C# on .NET Framework, Java 8, Delphi, C++).
  Do not introduce a newer runtime or a new package manager without asking.
- Wrap the anybill calls in one adapter (authentication, queue, retry,
  mapping) so the rest of the till does not depend on anybill types.
- Read base URL, client id and credentials from the till's existing
  configuration mechanism; never from constants.
- Surface anybill errors through the till's existing logging and error
  handling; include the `traceId`.
- Keep the mapping from the till's receipt model to `AddBillDto` in one
  place and cover it with a unit test per receipt type (sale, item
  discount, receipt discount, return, split payment, foreign currency,
  fiscal unit failure, known customer).

## Out of scope for AI agents

- Do not generate, guess or commit service account credentials, client ids
  or tokens. Production credentials are handed out by anybill after the
  integration review.
- Do not change fiscalisation (TSE, RKSV, BOI-TVA, TicketBAI) or tax
  calculation logic without explicit human review.
- Do not enable or assume merchant-specific features (transparent user
  creation, self-generated receipt ids, print buffer) – they are switched on
  by anybill per merchant.
- If the flow is unclear, fetch the relevant documentation page and ask the
  developer instead of assuming.
