# Accounting platforms

> One API over Dinero and e-conomic: what is identical, the fields that name a platform's own choice, and what a platform can refuse.

Source: https://www.tabba.io/developers/api/platforms

A key is bound to one connected company, and that company keeps its books in
one accounting system: **Dinero** or **e-conomic**. Which one it is, is not
something your integration has to know.

Every endpoint under `/api/v1` behaves the same on either: the same request
bodies, the same response shapes, the same status codes and the same error
codes. No field, no code and no message of the API names a platform. You write
the integration once, and it keeps working for a company on the other system.

This page is where the two are named, because the few differences below are
yours to act on.

## What stays the same [#what-stays-the-same]

* **Field names.** `platform_ref` and `platform_number` on an invoice, a
  credit note and an accrual entry are the handle and the number the
  accounting system gave the booked document, whichever system that is.
* **Error codes.** `PLATFORM_API_ERROR` is the `502` for an accounting system
  that could not be reached or did not answer. A refusal by the accounting
  system is data under `refusal`, with a reason code of Tabba's own and the
  platform's words beside it.
* **`send_error`.** `null` on a document whose delivery succeeded, and an
  object with a `reason` and the platform's own words (`platform_reason`) when
  the document was booked and its delivery is not known to have succeeded.
* **Amounts, dates, currencies, recognition and the month-end accrual** are
  Tabba's own arithmetic and are identical by construction.

## Ids are the platform's handles [#ids-are-the-platforms-handles]

A customer's and a product's `id` is the handle the accounting system knows
the record by: a GUID on Dinero, the customer or product number on e-conomic.
Treat it as opaque text. It is not always a UUID, and a number can be given to
another record once its own is deleted in the accounting system.

Address records by your own `external_id` and you never depend on the handle:
see [external\_id and idempotency](/developers/api/external-id).

## Fields that name a platform's own choice [#fields-that-name-a-platforms-own-choice]

A few fields of a customer and a product name an entry of a list the
accounting system keeps, or a fact only one system holds.

| Field                                    | On Dinero                                       | On e-conomic                                              |
| ---------------------------------------- | ----------------------------------------------- | --------------------------------------------------------- |
| `vat_zone_ref` (customer)                | not kept                                        | **required**: the VAT zone the customer is placed in      |
| `customer_group_ref` (customer)          | not kept                                        | **required**: the customer group the customer is filed in |
| `payment_terms_ref` (customer)           | not kept                                        | an entry of the company's list of payment terms           |
| `layout_ref` (customer)                  | not kept                                        | the customer's document layout                            |
| `payment_mean` (customer)                | **required** when `send_method` is `einvoice`   | not asked for                                             |
| `invoice_mail_out_option_key` (customer) | `VAT`, `GLN`, `SE` or `P`                       | `GLN`                                                     |
| `account_ref` (product)                  | the account number as text, and may be left out | the number of the product group the revenue is filed in   |

Where a required value is missing, the write is answered `422` with
`VALIDATION_FAILED`, and `field_errors` carries a code under the field's name:

```json
HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "type": "invalid_request_error",
    "code": "VALIDATION_FAILED",
    "message": "The customer failed validation.",
    "field_errors": {
      "vat_zone_ref": ["VAT_ZONE_REQUIRED"],
      "customer_group_ref": ["CUSTOMER_GROUP_REQUIRED"]
    },
    "request_id": "req_…"
  }
}
```

To find valid values, read a customer or a product the company already has
(`GET /customers`, `GET /products`): each states the references the
accounting system holds for it.

Two rules keep a sync that does not model these fields from moving a record:

* `POST /customers` under an `external_id` that is taken keeps `vat_zone_ref`,
  `payment_terms_ref`, `layout_ref` and `customer_group_ref` as they are when
  the body leaves them out.
* `POST /products` under an `external_id` that is taken keeps `account_ref`
  when the body leaves it out and `account_number` is unchanged.

A product whose `account_ref` names no product group of the company is refused
by the platform, with the reason under `refusal`.

## What a platform can refuse [#what-a-platform-can-refuse]

| Code                      | Status | What happened                                                                                                                                      |
| ------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PLATFORM_REFUSED`        | `422`  | The accounting system refused the write of a customer or a product. Nothing was written. `refusal` says why.                                       |
| `ISSUE_REFUSED`           | `422`  | The accounting system refused to book an invoice. Nothing was booked.                                                                              |
| `CREDIT_REFUSED`          | `422`  | The accounting system refused to book a credit note. Nothing was booked.                                                                           |
| `PLATFORM_NOT_CONFIGURED` | `409`  | The company's connection is missing a setting the write needs. Nothing about your request is wrong: an admin completes the setup in the workspace. |
| `PLATFORM_UNAVAILABLE`    | `409`  | The company has no accounting system to write to.                                                                                                  |
| `PLATFORM_API_ERROR`      | `502`  | The accounting system could not be reached, or did not answer. It may or may not hold the change.                                                  |

A refusal is not an outage. Do not retry a `422` with the same body: read
`refusal.reason` and `refusal.platform_reason`, and correct the record. Retry a
`502` with backoff. A create sent under an `external_id` is safe to send again
unchanged within the hour: see
[external\_id and idempotency](/developers/api/external-id).

## E-invoices [#e-invoices]

A customer whose `send_method` is `einvoice` states how it is routed, in
`invoice_mail_out_option_key`, and the identifier that channel reads. The
channels a platform can route are in the table above. A channel the company's
platform cannot route is refused when the customer is written, as
`VALIDATION_FAILED` with `MAILOUT_OPTION_UNAVAILABLE` under
`invoice_mail_out_option_key`.

The delivery channel is checked again when an invoice is issued, **before
anything is booked**: a customer that cannot be sent to is refused with
`INVALID_SEND_METHOD`, and one that lacks its e-invoice details with
`MISSING_EINVOICE_DETAILS`. Both are `422`, and in both the fix is on the
customer.

## Differences that never reach a refusal [#differences-that-never-reach-a-refusal]

* **A credit note on e-conomic is a booked invoice with negative amounts.**
  Crediting works on both platforms and `POST /credit-notes` is identical.
  What differs is the document that lands in the books.
* **e-conomic sends a document as part of booking it, and keeps no readable
  record of a send.** A document booked there therefore states
  `send_error: null`. On Dinero the send is a step of its own after the
  booking, and a failed one is reported in `send_error`.
* **A booked document cannot be sent again through the API**, on either
  platform. Following up on a failed delivery is done in the workspace.
