<!-- doc: get-started/introduction url: https://valora.spotahome.com/developers/get-started/introduction -->

# Get Started

Use this guide to make your first Valora API request and understand the conventions used across the documentation.

Valora exposes REST APIs for finance, account, invoice, request, and integration workflows. The API is designed for server-side integrations, internal tools, automation platforms, and AI agents that need predictable HTTP contracts.

## Base URLs

| Environment | Base URL | Use for |
| --- | --- | --- |
| Production | `https://valora.spotahome.com` | Live data and production workflows. |
| Staging / testing | `https://valora-testing.laravel.cloud` | Testing integrations before using production. |
| Sandbox API paths | `/api/sandbox/v1/...` | Sandbox-compatible customer and employee data paths where documented. |

Always use the full endpoint URL shown on each endpoint page. Some resources use production paths under `/api/v1/...`; sandbox-compatible resources may use `/api/sandbox/v1/...`.

## Choose an authentication method

Most integrations should use a long-lived API token unless the endpoint explicitly requires JWT authentication.

| Method | Best for | Token lifetime |
| --- | --- | --- |
| Long-lived API token | Server integrations, scheduled syncs, automation tools, and customer portal clients. | Valid until rotated or revoked. |
| JWT | Short-lived sessions, SPAs, and mobile clients. | 30 minutes by default; refreshable. |

Read [Authentication Overview](/developers/auth/introduction) before integrating, then follow [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) or [JWT Authentication](/developers/auth/jwt-authentication).

## Make your first request

1. Generate or obtain the token required by the endpoint.
2. Send `Accept: application/json` on every request.
3. Send the token in the `Authorization` header.
4. Start with a documented connection test when the endpoint supports `?test=connection`.
5. Handle pagination, validation errors, rate limits, and empty result sets as normal response paths.

```http
GET https://valora.spotahome.com/api/v1/c/transactions?test=connection
Authorization: Bearer {token}
Accept: application/json
```

Example success response:

```json
{
  "message": "You are connected!",
  "accepted_params": {
    "test": "Set to \"connection\" to test API connection"
  }
}
```

## Common headers

| Header | Required | Description |
| --- | --- | --- |
| `Accept: application/json` | Yes | Ensures validation, auth, and application errors are returned as JSON. |
| `Authorization: Bearer {token}` | Yes for protected endpoints | Sends either a long-lived API token or a JWT, depending on the endpoint. |
| `Content-Type: application/json` | Yes for JSON request bodies | Required when sending `POST`, `PUT`, `PATCH`, or other JSON payloads. |

## Common data conventions

| Convention | Details |
| --- | --- |
| Dates | Use `YYYY-MM-DD` unless an endpoint documents a datetime format. |
| Datetimes | Use ISO 8601 when datetimes are accepted or returned. |
| Currency | Currency codes use ISO 4217 values such as `EUR`. |
| Amounts | Response amounts are commonly returned in minor units, such as cents. Endpoint pages document any filters that accept major units. |
| Pagination | List endpoints commonly use `page` and `per_page`. Defaults and maximums are documented per endpoint. |
| Empty results | Some list endpoints return `404 Not Found` with an empty `data` array and pagination metadata. Treat this as a handled no-results state where documented. |

## Documentation formats

Every public documentation page is available for both humans and tools:

| Format | URL pattern |
| --- | --- |
| Web page | `/developers/{page}` |
| Raw Markdown | `/developers/{page}.md` |
| Per-page OpenAPI | `/developers/{page}.openapi.json` when the page describes an endpoint |
| Aggregated OpenAPI | `/developers/openapi.json` |
| AI discovery index | `/llms.txt` |
| Full AI context bundle | `/llms-full.txt` |

## Recommended integration order

1. Read [Authentication Overview](/developers/auth/introduction).
2. Generate a sandbox token if the resource supports sandbox paths.
3. Call a connection test endpoint.
4. Implement normal success and pagination handling.
5. Add handling for validation errors, unauthorized responses, forbidden responses, and rate limits.
6. Move to production only after confirming request headers, token class, and response parsing.



---

<!-- doc: get-started/ai-and-openapi url: https://valora.spotahome.com/developers/get-started/ai-and-openapi -->

# AI And OpenAPI

Valora documentation is available in formats that are easy for developers, automation tools, and AI agents to consume.

## Discovery files

| File | Description |
| --- | --- |
| `/llms.txt` | A compact Markdown index of public developer documentation pages. |
| `/llms-full.txt` | A larger Markdown bundle containing public developer documentation content. |
| `/developers/openapi.json` | Aggregated OpenAPI index for documented API endpoints. |

## Per-page formats

Each documentation page can be requested as a web page or raw Markdown.

| Format | Example |
| --- | --- |
| Web | `/developers/get-started/introduction` |
| Markdown | `/developers/get-started/introduction.md` |

Endpoint pages may also expose a focused OpenAPI document:

```text
/developers/customer/transactions.openapi.json
```

## AI integration guidance

When using an AI assistant or agent to build against Valora:

- Give it the relevant endpoint page, not only the homepage.
- Include the authentication page for the token type you plan to use.
- Include this Get Started section for shared conventions.
- Ask it to preserve required headers and documented response handling.
- Do not provide real tokens, passwords, or personal data to public AI tools.

## Minimal prompt context

For most endpoint implementation tasks, provide:

1. The raw Markdown URL for the endpoint.
2. The raw Markdown URL for the authentication method.
3. The raw Markdown URL for [Errors](/developers/get-started/errors.md).
4. The raw Markdown URL for [Rate Limits](/developers/get-started/rate-limits.md).



---

<!-- doc: get-started/authentication url: https://valora.spotahome.com/developers/get-started/authentication -->

# Authentication

Valora uses bearer authentication. The endpoint page tells you which token type is accepted.

## Authentication methods

| Method | Header | Use when |
| --- | --- | --- |
| Long-lived API token | `Authorization: Bearer vl_...` or `Authorization: Bearer sd_...` | The endpoint requires long-lived API token authentication. |
| JWT | `Authorization: Bearer eyJ...` | The endpoint requires JWT authentication. |

Do not mix token types. A JWT will not authenticate an endpoint that expects a long-lived API token, and a long-lived API token will not authenticate an endpoint that expects a JWT.

## Token audiences

Some endpoints are restricted by account type.

| Audience | Meaning |
| --- | --- |
| Customer | The token belongs to a customer account and can access customer-scoped resources. |
| Employee | The token belongs to an employee account and can access employee-scoped resources where permitted. |

If the token is valid but belongs to the wrong audience, the API returns `403 Forbidden`.

## Required headers

```http
Authorization: Bearer {token}
Accept: application/json
```

Use `Content-Type: application/json` when sending a JSON request body.

## Token safety

- Store tokens in a secrets manager or encrypted environment variable.
- Never put tokens in URLs.
- Never commit tokens to source control.
- Rotate tokens when team access changes or a token may have been exposed.
- Prefer sandbox tokens while developing or testing.

> **Warning**  
> For security reasons, we do not store the plain-text value of generated tokens.  
> Make sure to copy and store your token securely when it is created, as you will not be able to view it again.

## Next steps

- Read [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) for production and sandbox API tokens.
- Read [JWT Authentication](/developers/auth/jwt-authentication) for short-lived session tokens.



---

<!-- doc: get-started/environments url: https://valora.spotahome.com/developers/get-started/environments -->

# Environments

Valora documents production and testing hosts separately from API path versions. Always combine the documented host with the exact path shown on the endpoint page.

## Hosts

| Environment | Host |
| --- | --- |
| Production | `https://valora.spotahome.com` |
| Staging / testing | `https://valora-testing.laravel.cloud` |

## Path families

| Path family | Description |
| --- | --- |
| `/api/v1/...` | Versioned production API paths. |
| `/api/sandbox/v1/...` | Sandbox-compatible API paths where the endpoint supports sandbox data. |
| `/api/auth/jwt/...` | JWT login, refresh, user info, and logout paths. |
| `/api/v1/tokens/...` | Long-lived API token lifecycle paths. |

## Production vs sandbox tokens

Long-lived production and sandbox tokens are separate credentials. A production token starts with `vl_`; a sandbox token starts with `sd_`.

Generating, rotating, or revoking one environment's token does not change the other environment's token.

## Moving to production

Before switching to production:

- Replace sandbox URLs with the documented production endpoint URLs.
- Replace sandbox tokens with production tokens.
- Confirm that your integration handles `401`, `403`, `404`, `422`, and `429` responses.
- Confirm that automated jobs have retry behavior for temporary `5xx` responses.



---

<!-- doc: get-started/errors url: https://valora.spotahome.com/developers/get-started/errors -->

# Errors

Valora uses standard HTTP status codes and JSON error responses.

## Common status codes

| Status | Meaning | Recommended handling |
| --- | --- | --- |
| `400 Bad Request` | The request is malformed or unsupported. | Check the endpoint documentation and request shape. |
| `401 Unauthorized` | The token is missing, invalid, expired, or the wrong token type. | Re-authenticate, refresh, or replace the token. |
| `403 Forbidden` | The token is valid but not allowed to access the resource. | Confirm account type, permissions, and environment. |
| `404 Not Found` | The resource was not found, or no records matched filters where documented. | Check identifiers and treat documented empty list responses as no-results states. |
| `422 Unprocessable Entity` | Validation failed. | Read the `errors` object and correct the submitted fields. |
| `429 Too Many Requests` | Rate limit exceeded. | Back off and retry later. |
| `500 Internal Server Error` | Unexpected server error. | Retry later for idempotent requests and contact support if it persists. |
| `503 Service Unavailable` | Temporary unavailability. | Retry with backoff. |

## Validation errors

Validation failures return `422 Unprocessable Entity`.

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "email": [
      "The email field is required."
    ]
  }
}
```

## Authentication errors

Missing or invalid bearer credentials return `401 Unauthorized`.

```json
{
  "message": "Unauthenticated."
}
```

Valid credentials with the wrong account type or insufficient access return `403 Forbidden`.

```json
{
  "message": "Access denied."
}
```

## Operational errors

For `429`, `500`, and `503` responses, clients should avoid immediate tight retry loops. Use exponential backoff with a maximum retry count and log the failed request metadata without logging secrets.



---

<!-- doc: get-started/pagination-and-filtering url: https://valora.spotahome.com/developers/get-started/pagination-and-filtering -->

# Pagination And Filtering

List endpoints commonly support pagination and filters. Each endpoint page documents the parameters it accepts.

## Pagination parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `page` | integer | Page number to return. Defaults are documented per endpoint. |
| `per_page` | integer | Number of records per page. Defaults and maximums are documented per endpoint. |

Example:

```http
GET https://valora.spotahome.com/api/v1/c/transactions?page=2&per_page=100
Authorization: Bearer {token}
Accept: application/json
```

## Pagination response

```json
{
  "pagination": {
    "total": 123,
    "per_page": 100,
    "current_page": 2,
    "last_page": 2,
    "from": 101,
    "to": 123
  },
  "data": []
}
```

## Filtering

Filters are passed as query string parameters.

```http
GET https://valora.spotahome.com/api/v1/c/transactions?pay_date_from=2026-01-01&pay_date_to=2026-01-31
Authorization: Bearer {token}
Accept: application/json
```

## Filter conventions

| Convention | Example |
| --- | --- |
| Date range start | `pay_date_from=2026-01-01` |
| Date range end | `pay_date_to=2026-01-31` |
| Amount minimum | `amount_from=100.50` |
| Amount maximum | `amount_to=500.00` |
| Exact booking reference | `booking_id=BK123456` |

## No results

Some endpoints return `404 Not Found` when no records match the filters, while still returning an empty `data` array and pagination metadata. Endpoint pages document this behavior when it applies.

Clients should treat documented no-results responses as a normal empty state, not as an integration failure.



---

<!-- doc: get-started/rate-limits url: https://valora.spotahome.com/developers/get-started/rate-limits -->

# Rate Limits

Valora rate limits API traffic to protect service stability and keep access fair across integrations.

## Default limits

| Request type | Limit |
| --- | --- |
| Unauthenticated requests | 60 requests per minute per IP address. |
| Authenticated customer requests | 120 requests per minute per customer. |
| Authenticated employee requests | At least 2000 requests per minute per employee. The default is 5000 requests per minute. |

Endpoint pages may document more specific limits when they differ.

## Plan-based limits

Customer accounts may be assigned a plan with additional windows, such as hourly, daily, weekly, or monthly quotas. When multiple windows apply, every request must fit within all active windows.

For example, a plan can allow:

```json
{
  "minute": 120,
  "hour": 5000,
  "day": 50000,
  "month": 1000000
}
```

If any one window is exhausted, the API returns `429 Too Many Requests` until that window resets.

## Check your current limits

Authenticated API token owners can check their current usage and reset times.

```http
GET https://valora.spotahome.com/api/v1/tokens/limits
Authorization: Bearer {token}
Accept: application/json
```

Example response:

```json
{
  "message": "Rate limits retrieved successfully",
  "rate_limits": {
    "audience": "customer",
    "plan": {
      "id": 12,
      "name": "Business",
      "slug": "business"
    },
    "limits": [
      {
        "window": "minute",
        "limit": 120,
        "used": 42,
        "remaining": 78,
        "resets_in_seconds": 31,
        "resets_at": "2026-05-03T22:30:00+02:00"
      }
    ]
  }
}
```

## Rate limit response

When a client exceeds its limit, the API returns `429 Too Many Requests`.

```json
{
  "message": "Rate limit exceeded.",
  "rate_limit": {
    "window": "minute",
    "limit": 120,
    "used": 120,
    "remaining": 0,
    "resets_in_seconds": 31,
    "resets_at": "2026-05-03T22:30:00+02:00"
  }
}
```

## Client behavior

- Back off when receiving `429`.
- Retry after a delay instead of retrying immediately.
- Add jitter to automated retries so many jobs do not retry at the same time.
- Cache stable reference data where appropriate.
- Avoid polling more frequently than the workflow requires.

## Suggested retry strategy

For automated integrations:

| Attempt | Delay |
| --- | --- |
| 1 | 30 seconds |
| 2 | 60 seconds |
| 3 | 2 minutes |
| 4 | 5 minutes |

Stop retrying after repeated failures and surface the issue to an operator or monitoring system.


---

<!-- doc: get-started/requests-and-responses url: https://valora.spotahome.com/developers/get-started/requests-and-responses -->

# Requests And Responses

Valora APIs use predictable HTTP methods, JSON request bodies, and JSON responses.

## Request basics

| Item | Convention |
| --- | --- |
| Headers | Send `Accept: application/json` on every request. |
| JSON bodies | Send `Content-Type: application/json`. |
| Dates | Use `YYYY-MM-DD` unless the endpoint documents another format. |
| Booleans | Use JSON booleans `true` and `false` in request bodies. |
| Filters | Use query string parameters for list filters. |

## Success responses

Successful responses usually include a human-readable `message` and a `data` object or array.

```json
{
  "message": "Transactions retrieved successfully.",
  "data": []
}
```

List endpoints may also include pagination metadata.

```json
{
  "message": "Transactions retrieved successfully.",
  "pagination": {
    "total": 123,
    "per_page": 100,
    "current_page": 1,
    "last_page": 2,
    "from": 1,
    "to": 100
  },
  "data": []
}
```

## Amounts and currency

When an endpoint returns monetary values, amounts are commonly returned in minor units such as cents. For example, `10000` means `100.00` in the response currency.

Endpoint pages document exceptions, including filters that accept major units such as `100.50`.

## Connection tests

Some endpoints support `?test=connection`.

```http
GET https://valora.spotahome.com/api/v1/c/transactions?test=connection
Authorization: Bearer {token}
Accept: application/json
```

Use connection tests to verify:

- The host is reachable.
- The bearer token is valid.
- The token has the right audience for the endpoint.
- Your client sends the required headers.



---

<!-- doc: auth/api-key-authentication url: https://valora.spotahome.com/developers/auth/api-key-authentication -->

# Long-Lived API Token Authentication

How to generate, use, verify, and revoke Valora's long-lived bearer tokens for routes protected by the `auth:api` guard.

---

## Overview

A long-lived API token is a bearer credential tied to your user record (`vl_…` for production, `sd_…` for sandbox). It stays valid until you replace it by generating a new one or revoke it. Use it on any route that requires the `auth:api` guard.

> **API hosts:** 
>
> - Production `https://valora.spotahome.com/` (sandbox compatible);
> - Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible). 
>
> Token lifecycle routes use `/api/v1/tokens/...` on either host. Customer and employee **data** in the sandbox *environment* use `/api/sandbox/v1/...` (typically on the staging host).

You can obtain a token in either of these ways:

1. **Web app (Settings)** — While logged into Valora in the browser, use **Settings → API Tokens** to mint or rotate tokens without calling the public generate endpoints. Details: [From Settings (browser)](#from-settings-browser).
2. **HTTP API** — Exchange **email + password** (no prior bearer token) via `POST https://valora.spotahome.com/api/v1/tokens/generate` or `POST https://valora.spotahome.com/api/v1/tokens/generate/sandbox`, or call the authenticated employee-only token endpoints if you already have a session token (documented in the tables below).

There are two separate tokens — one for **production** and one for **sandbox**. They are independent: generating or revoking one does not affect the other.

> **Note:** Valora stores only a **hash** of the token in the database. The plain token is shown **once** when it is generated (in the browser UI or in the JSON response). Copy it immediately and store it securely.

---

## Generate a token

### From Settings (browser)

For people using the Valora web UI:

1. Sign in with your normal Valora account (session auth).
2. Open **Settings → API Tokens** in the browser.
3. From here you can click on **Generate Production Token** or **Generate Sandbox Token**.
4. Each successful generation writes a new `vl_…` or `sd_…` token and **replaces** the previous hash for that environment on your user record (any old bearer of that environment stops working).
5. Copy the value from the read-only field before leaving the page; it is not shown again.

This path does **not** require posting your password to the public `POST https://valora.spotahome.com/api/v1/tokens/generate` endpoints; it requires an existing web session (`auth` middleware on the settings route group).

### Production token (HTTP)

Exchange your credentials for a production token.

| Method | URL | Auth required |
|--------|-----|---------------|
| `POST` | `https://valora.spotahome.com/api/v1/tokens/generate` | No |

**Request**

```http
POST https://valora.spotahome.com/api/v1/tokens/generate
Accept: application/json
Content-Type: application/json

{
  "email": "you@example.com",
  "password": "your-password"
}
```

**Request body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | string | Yes | Your account email address. |
| `password` | string | Yes | Your account password. |

**Success response — `200 OK`**

```json
{
  "message": "API token generated successfully",
  "api_token": "vl_AbCdEfGhIjKlMnOpQrStUvWxYz...",
  "environment": "production",
  "note": "Please save this token in a secure location. You will not be able to access it again."
}
```

---

### Sandbox token (HTTP)

Same HTTP flow as production, but issues a sandbox token scoped to the sandbox environment. You can also generate the sandbox token from **Settings → API Tokens** in the browser (see [From Settings (browser)](#from-settings-browser)).

| Method | URL | Auth required |
|--------|-----|---------------|
| `POST` | `https://valora.spotahome.com/api/v1/tokens/generate/sandbox` | No |

**Request**

```http
POST https://valora.spotahome.com/api/v1/tokens/generate/sandbox
Accept: application/json
Content-Type: application/json

{
  "email": "you@example.com",
  "password": "your-password"
}
```

**Success response — `200 OK`**

```json
{
  "message": "Sandbox API token generated successfully",
  "api_token": "sd_SbCdEfGhIjKlMnOpQrStUvWxYz...",
  "environment": "sandbox",
  "note": "Please save this token in a secure location. You will not be able to access it again."
}
```

---

## Use your token

Send the token as a bearer credential on every request:

```http
GET https://valora.spotahome.com/api/v1/c/transactions
Authorization: Bearer vl_AbCdEfGhIjKlMnOpQrStUvWxYz...
Accept: application/json
```

---

## Verify your token

Confirm that your current token is valid and check which environment it belongs to.

| Method | URL | Auth required |
|--------|-----|---------------|
| `GET` | `https://valora.spotahome.com/api/v1/tokens/verify` | Yes (`auth:api`) |

**Request**

```http
GET https://valora.spotahome.com/api/v1/tokens/verify
Authorization: Bearer vl_AbCdEfGhIjKlMnOpQrStUvWxYz...
Accept: application/json
```

**Success response — `200 OK`**

```json
{
  "message": "Token is valid",
  "environment": "production"
}
```

The `environment` field is either `"production"` or `"sandbox"` depending on which token you sent.

---

## Check rate limits

Retrieve the active rate-limit plan, usage, remaining requests, and reset time for the token owner.

| Method | URL | Auth required |
|--------|-----|---------------|
| `GET` | `https://valora.spotahome.com/api/v1/tokens/limits` | Yes (`auth:api`) |

**Request**

```http
GET https://valora.spotahome.com/api/v1/tokens/limits
Authorization: Bearer vl_AbCdEfGhIjKlMnOpQrStUvWxYz...
Accept: application/json
```

**Success response — `200 OK`**

```json
{
  "message": "Rate limits retrieved successfully",
  "rate_limits": {
    "audience": "customer",
    "plan": {
      "id": 12,
      "name": "Business",
      "slug": "business"
    },
    "limits": [
      {
        "window": "minute",
        "limit": 120,
        "used": 42,
        "remaining": 78,
        "resets_in_seconds": 31,
        "resets_at": "2026-05-03T22:30:00+02:00"
      }
    ]
  }
}
```

---

## Revoke a token

### Auto-detect environment

Revoke the token you are currently using. The endpoint detects the environment from the token automatically.

| Method | URL | Auth required |
|--------|-----|---------------|
| `POST` | `https://valora.spotahome.com/api/v1/tokens/revoke` | Yes (`auth:api`) |

**Request**

```http
POST https://valora.spotahome.com/api/v1/tokens/revoke
Authorization: Bearer vl_AbCdEfGhIjKlMnOpQrStUvWxYz...
Accept: application/json
```

**Success response — `200 OK`**

```json
{
  "message": "production API token revoked successfully",
  "status": "revoked",
  "environment": "production"
}
```

**Already revoked — `200 OK`**

```json
{
  "message": "No active production API token found. Token may have already been revoked.",
  "status": "already_revoked",
  "environment": "production"
}
```

---

### Revoke production explicitly

Force-revoke your production token regardless of which token you are sending.

| Method | URL | Auth required |
|--------|-----|---------------|
| `POST` | `https://valora.spotahome.com/api/v1/tokens/revoke/production` | Yes (`auth:api`) |

**Request**

```http
POST https://valora.spotahome.com/api/v1/tokens/revoke/production
Authorization: Bearer vl_AbCdEfGhIjKlMnOpQrStUvWxYz...
Accept: application/json
```

**Success response — `200 OK`**

```json
{
  "message": "Production API token revoked successfully",
  "status": "revoked",
  "environment": "production"
}
```

---

### Revoke sandbox explicitly

Force-revoke your sandbox token.

| Method | URL | Auth required |
|--------|-----|---------------|
| `POST` | `https://valora.spotahome.com/api/v1/tokens/revoke/sandbox` | Yes (`auth:api`) |

**Request**

```http
POST https://valora.spotahome.com/api/v1/tokens/revoke/sandbox
Authorization: Bearer vl_AbCdEfGhIjKlMnOpQrStUvWxYz...
Accept: application/json
```

**Success response — `200 OK`**

```json
{
  "message": "Sandbox API token revoked successfully",
  "status": "revoked",
  "environment": "sandbox"
}
```

---

## Re-generate while authenticated

If you already have a valid token and want to rotate it without re-entering your password, use these authenticated endpoints. They issue a fresh token and invalidate the previous one.

| Method | URL | Token type issued |
|--------|-----|-------------------|
| `POST` | `https://valora.spotahome.com/api/v1/tokens/generate/production` | Production |
| `POST` | `https://valora.spotahome.com/api/v1/tokens/generate/sandbox/token` | Sandbox |

**Request (production)**

```http
POST https://valora.spotahome.com/api/v1/tokens/generate/production
Authorization: Bearer vl_AbCdEfGhIjKlMnOpQrStUvWxYz...
Accept: application/json
```

**Success response — `200 OK`**

```json
{
  "message": "API token generated successfully",
  "api_token": "vl_NewTokenValueHere...",
  "environment": "production"
}
```

> **Note:** These endpoints require `auth:api` with a valid token of any environment. Unlike the public generate endpoints, they do **not** include the one-time-view note because you are already authenticated when you call them.

---

## Errors

### `401 Unauthorized` — missing or invalid token

```json
{
  "message": "Unauthenticated."
}
```

You sent a request to a protected endpoint without a token, or with an expired/revoked one.

### `422 Unprocessable Entity` — validation failure

```json
{
  "message": "The email field is required.",
  "errors": {
    "email": ["The email field is required."]
  }
}
```

One or more required fields in your request body are missing or invalid.

### `422 Unprocessable Entity` — wrong credentials

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "email": ["The provided credentials are incorrect."]
  }
}
```

Your email/password combination did not match any account.


---

<!-- doc: auth/introduction url: https://valora.spotahome.com/developers/auth/introduction -->

# Authentication Overview

Valora uses two authentication methods depending on the endpoint you are calling.

> **API hosts:** 
> 
> - Production `https://valora.spotahome.com/` (sandbox compatible);
> - Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible).

## Authentication methods

| Method | Guard | Token lifetime | When to use |
|--------|-------|---------------|-------------|
| Long-lived API token | `auth:api` | Until revoked | Service integrations, automated jobs, customer portal clients |
| JWT | `auth:api_jwt` | 30 minutes (refreshable) | Short-lived sessions, SPAs, mobile clients |
| Passkeys | `web` | Session-based | Modern, passwordless browser-based sign-in |

## How authentication is determined

Each endpoint specifies which authentication method it requires. The endpoint documentation states the guard and token class. Sending a **JWT** to an `auth:api` route (or vice versa) typically returns **401**. Using a valid token of the **wrong class** (customer vs employee) on class-restricted routes returns **403** from the Valora API middleware — see that endpoint's Authentication section.

## Common rules

- Always send `Accept: application/json` on every request.
- Send your token in the `Authorization: Bearer {token}` header.
- Never put tokens in URLs or commit them to source control.
- Treat long-lived tokens like passwords — rotate them when compromised.
- **HTTP rate limits:** All `/api/*` routes use Laravel's `api` middleware group (`throttle:api`). Defaults are documented in [Long-Lived API Token Authentication](auth/API-key-authentication) and [JWT Authentication](auth/JWT-authentication) (guest vs authenticated buckets).
- **Wrong token class:** A missing or invalid bearer token usually yields **401** `Unauthenticated.` from the guard. A **valid** token for the wrong audience (for example a customer token on an employee-only route) yields **403** from `CheckEmployeeApi` / `CheckCustomerApi` — see the endpoint's Authentication table.

## HTTP rate limiting

All requests to Valora API endpoints are rate limited to ensure fair usage and system stability. Limits vary depending on whether the request is authenticated and the type of user.

| Authenticated | User type | Rate | Every |
|--------|-------|---------------|-------------|
| No | - | 60 | per minute per IP |
| Yes | `customer` | 120 | per minute |
| Yes | `employee` | 5000 | per minute |

**Notes**
- Unauthenticated requests are limited per IP address.
- Authenticated requests are limited based on the user making the request.
- If a limit is exceeded, the API will return a 429 Too Many Requests response.

## Endpoint guides

- [Long-lived API token](auth/API-key-authentication)
- [JWT authentication](auth/JWT-authentication)
- [Passkey authentication](auth/passkeys)


---

<!-- doc: auth/jwt-authentication url: https://valora.spotahome.com/developers/auth/jwt-authentication -->

# JWT Authentication

How to log in, use, refresh, inspect, and log out of Valora's JWT-based API for routes protected by the `auth:api_jwt` guard.

---

## Overview

JWTs are short-lived access tokens with a default TTL of **30 minutes**. When a token expires you must either refresh it (before or shortly after expiry) or log in again to get a new one.

Unlike the long-lived API token, a JWT is not stored on the server — it is self-contained. Refreshing it issues a new token and blacklists the old one immediately.

Use JWTs for routes that require `auth:api_jwt`. Do not send JWTs to `auth:api` routes, and do not send long-lived API tokens to `auth:api_jwt` routes.

> **API hosts:** Production `https://valora.spotahome.com/` (sandbox compatible); Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible). JWT routes live under `/api/auth/jwt/...` on either host.

## HTTP rate limiting

All routes in `routes/api.php` sit behind Laravel's `api` middleware group, which applies **`throttle:api`**. `POST https://valora.spotahome.com/api/auth/jwt/login` (no bearer token yet) counts as a **guest** request (**60 requests per minute per IP** by default; `API_RATE_LIMIT_GUEST_PER_MINUTE`). Authenticated `auth:api_jwt` requests use the per-user limits for **customers** (120/min default) or **employees** (5000/min default); see `config/valora.php` and `AppServiceProvider::configureRateLimiting`.

---

## Log in

Exchange your credentials for a JWT.

| Method | URL | Auth required |
|--------|-----|---------------|
| `POST` | `https://valora.spotahome.com/api/auth/jwt/login` | No |

**Request**

```http
POST https://valora.spotahome.com/api/auth/jwt/login
Accept: application/json
Content-Type: application/json

{
  "email": "you@example.com",
  "password": "your-password"
}
```

**Request body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | string | Yes | Your account email address. |
| `password` | string | Yes | Your account password. |

**Success response — `200 OK`**

```json
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS52YWxvcmEuY29tIiwiaWF0IjoxNzA5MDAwMDAwLCJleHAiOjE3MDkwMDE4MDAsIm5iZiI6MTcwOTAwMDAwMCwianRpIjoiQWJDZEVmR2hJaksiLCJzdWIiOiI0MiIsInBydiI6IjIzYmQ1Yzg5NDlmNjAwYWRiMzllNzAxYzQwMDg3MmRiN2E1OTc2ZjcifQ.SIGNATURE",
  "token_type": "bearer",
  "expires_in_minutes": 30
}
```

| Field | Type | Description |
|-------|------|-------------|
| `access_token` | string | The JWT to use in all subsequent requests. |
| `token_type` | string | Always `"bearer"`. |
| `expires_in_minutes` | integer | How many minutes until the token expires (default: `30`). |

---

## Use your token

Pass the JWT in the `Authorization` header on every request to a `auth:api_jwt` protected route:

```http
GET https://valora.spotahome.com/api/auth/jwt/me
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Accept: application/json
```

---

## Get your user info

Confirm your identity and retrieve basic profile data for the token's owner.

| Method | URL | Auth required |
|--------|-----|---------------|
| `GET` | `https://valora.spotahome.com/api/auth/jwt/me` | Yes (`auth:api_jwt`) |

**Request**

```http
GET https://valora.spotahome.com/api/auth/jwt/me
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Accept: application/json
```

**Success response — `200 OK`**

```json
{
  "email": "you@example.com",
  "name": "Jane",
  "family_name": "Doe",
  "role": "finance_member
}
```

| Field | Type | Description |
|-------|------|-------------|
| `email` | string | Your email address. |
| `name` | string | Your first name. |
| `family_name` | string | Your last name. |
| `role` | string | Your role. |

---

## Refresh your token

Issue a new token before (or just after) the current one expires. The old token is immediately blacklisted — replace it on your end after calling this endpoint.

| Method | URL | Auth required |
|--------|-----|---------------|
| `POST` | `https://valora.spotahome.com/api/auth/jwt/refresh` | Yes (`auth:api_jwt`) |

**Request**

```http
POST https://valora.spotahome.com/api/auth/jwt/refresh
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Accept: application/json
```

**Success response — `200 OK`**

```json
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.NEW_PAYLOAD.NEW_SIGNATURE",
  "token_type": "bearer",
  "expires_in_minutes": 30
}
```

Replace your stored token with the new `access_token` immediately. The old one cannot be used again.

> **Note:** If the token has already expired beyond the refresh grace period, you will receive a `401` and must log in again.

---

## Log out

Blacklist the current token and end your session. Discard the token on your end after calling this — it will not work again.

| Method | URL | Auth required |
|--------|-----|---------------|
| `POST` | `https://valora.spotahome.com/api/auth/jwt/logout` | Yes (`auth:api_jwt`) |

**Request**

```http
POST https://valora.spotahome.com/api/auth/jwt/logout
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Accept: application/json
```

**Success response — `200 OK`**

```json
{
  "message": "Successfully logged out"
}
```

---

## Errors

### `401 Unauthorized` — invalid credentials

Returned by the login endpoint when your email or password is wrong.

```json
{
  "message": "Invalid credentials"
}
```

### `401 Unauthorized` — expired token

```json
{
  "message": "Token has expired"
}
```

Call `POST https://valora.spotahome.com/api/auth/jwt/refresh` to get a new token, or log in again if the grace period has passed.

### `401 Unauthorized` — invalid token

```json
{
  "message": "Token is invalid"
}
```

The token is malformed or was blacklisted. Log in again to get a fresh one.

### `401 Unauthorized` — missing token

```json
{
  "message": "Token absent or invalid"
}
```

No `Authorization` header was sent, or the value could not be parsed as a JWT.

### `422 Unprocessable Entity` — validation failure

```json
{
  "message": "The email field is required.",
  "errors": {
    "email": ["The email field is required."],
    "password": ["The password field is required."]
  }
}
```

### `500 Internal Server Error` — token creation failure

```json
{
  "message": "Could not create token"
}
```

An unexpected error occurred on the server while signing the JWT. Retry the request. If the error persists, contact support.


---

<!-- doc: auth/passkeys url: https://valora.spotahome.com/developers/auth/passkeys -->

# Passkey Authentication

Passkeys are a modern, secure way to sign in to Valora without using a password. They use your device's biometric sensors (like Fingerprint or Face ID) or a security key.

---

## Overview

Passkeys are more secure than passwords because they are unique to Valora and cannot be guessed or stolen via phishing. When you use a passkey, your device proves it has the correct key without ever sending your sensitive biometric data to our servers.

Use passkeys for:
- Fast and secure login to the Valora web application.
- Adding an extra layer of security to your account.

> **Requirements:**
> - A modern browser (Chrome, Safari, Firefox, or Edge).
> - A device with biometrics (TouchID, FaceID, Windows Hello) or a physical security key (like a YubiKey).
> - **HTTPS is required** for all passkey operations.

---

## Setting up a Passkey

You can register a new passkey from your account settings.

1. Sign in to Valora using your email and password.
2. Navigate to **Settings → Security → Passkeys**.
3. Click on **Register New Passkey**.
4. Follow your browser's prompts to use your biometric sensor or security key.
5. Give your passkey a friendly name (e.g., "Work Laptop" or "Personal Phone") to help you identify it later.

---

## Signing in with a Passkey

Once you have registered a passkey, you can use it to sign in quickly.

1. Go to the Valora login page.
2. Select the **Sign in with Passkey** option.
3. Your browser will prompt you to verify your identity.
4. Once verified, you will be logged in immediately.

---

## For Developers (API Flow)

If you are building a client that interacts with Valora's passkey system, the flow involves two steps for both registration and authentication:

### Registration Flow

1. **Request Options:** `POST /auth/passkey/register/options`
   - Returns the configuration required by the browser's WebAuthn API.
2. **Submit Response:** `POST /auth/passkey/register`
   - Sends the data generated by the browser back to Valora to finish registration.

### Authentication Flow

1. **Request Options:** `POST /auth/passkey/authenticate/options`
   - Returns a "challenge" that the browser needs to sign.
2. **Submit Response:** `POST /auth/passkey/authenticate`
   - Verifies the signature and logs the user in.

---

## Managing Passkeys

You can view all your registered passkeys in **Settings → Security → Passkeys**. From there, you can:
- Rename an existing passkey.
- Delete a passkey if you no longer have access to the device.

> **Note:** If you lose the device associated with a passkey, you can still sign in using your email and password (or another passkey) and then remove the old one.


---

<!-- doc: customer/conciliation-by-booking url: https://valora.spotahome.com/developers/customer/conciliation-by-booking -->

# Conciliation by Booking

Returns a consolidated financial conciliation for a single booking reference, combining invoices, credit notes, penalties, debt, and completed transactions.

Shared conventions and base URLs live in [Customer API Documentation](./introduction).

> **API hosts:** Production `https://valora.spotahome.com/` (sandbox compatible); Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible).

**Endpoint (production):** `GET https://valora.spotahome.com/api/v1/c/conciliation/booking`
**Endpoint (sandbox):** `GET https://valora-testing.laravel.cloud/api/sandbox/v1/c/conciliation/booking`

---

## Authentication

| Requirement | Details |
| --- | --- |
| **Type** | Bearer token (long-lived API token, `auth:api` guard) |
| **How to obtain a token** | See [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) |
| **Header** | `Authorization: Bearer {token}` |
| **Accept** | `Accept: application/json` |
| **Token class** | Customer only. Non-customer users receive HTTP 403 with message `Access denied. This resource is restricted to customers.` from `CheckCustomerApi` middleware. |
| **Rate limit** | 120 requests per minute per authenticated customer. |

---

## Connection test

```http
GET https://valora.spotahome.com/api/v1/c/conciliation/booking
Authorization: Bearer {token}
Accept: application/json
```

Sending a request with no query parameters returns the accepted parameters payload.

**Response:** `200 OK`

```json
{
  "message": "You are connected!",
  "accepted_params": {
    "booking_id": "string, required (exact booking reference)"
  }
}
```

---

## Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `booking_id` | string | Yes | Exact booking reference to conciliate. |

---

## Success response

**Status:** `200 OK`

```json
{
  "reference": "BK123456",
  "totals": {
    "transactions": 2,
    "transactions_made": 150000,
    "invoices_gross": 200000,
    "credit_notes_gross": 0,
    "penalties_gross": 3000,
    "debt": 1000
  },
  "invoicing": {
    "invoices": [
      {
        "document_id": "INV-2025-001",
        "doc_type": "Invoice",
        "invoice_to": "John Smith",
        "issue_date": "2025-01-10",
        "amount_net": 165000,
        "amount_gross": 200000,
        "currency": "EUR"
      }
    ],
    "credit_notes": []
  },
  "penalties": [
    {
      "id": "uuid-penalty-123",
      "booking_id": "BK123456",
      "status": "notify",
      "invoice_number": "PEN-2025-001",
      "amount": 3000,
      "currency": "EUR",
      "due_date": "2025-02-01"
    }
  ],
  "transactions": [
    {
      "booking_id": "BK123456",
      "beneficiary": "John Smith",
      "type": "Transfer",
      "amount": 150000,
      "issued_on": "2025-01-15",
      "currency": "EUR"
    }
  ]
}
```

### Response fields

#### Top-level

| Field | Type | Description |
|-------|------|-------------|
| `reference` | string | The requested booking reference. |
| `totals` | object | Aggregated monetary totals for this booking. |
| `invoicing` | object | Grouped invoices and credit notes. |
| `penalties` | array | Penalty records associated with this booking. |
| `transactions` | array | Completed payment transactions for this booking. |

#### `totals` object

| Field | Type | Description |
|-------|------|-------------|
| `transactions` | integer | Count of completed transactions. |
| `transactions_made` | integer | Sum of all transaction amounts in minor units (cents). |
| `invoices_gross` | integer | Total gross amount of invoices in minor units. |
| `credit_notes_gross` | integer | Total gross amount of credit notes in minor units. |
| `penalties_gross` | integer | Total of active (non-waived, non-pending) penalties in minor units. |
| `debt` | integer | Sum of pending debt records in minor units. |

---

## Errors

| Status | Meaning |
|--------|---------|
| `422 Unprocessable Entity` | `booking_id` parameter is missing. |
| `404 Not Found` | No records exist for the given booking reference. |

**422 — missing booking_id:**

```json
{
  "message": "The booking_id parameter is required."
}
```

**404 — no records found:**

```json
{
  "message": "No records found for this booking reference."
}
```

---

## Notes

- All monetary values are in **minor units** (cents).
- Invoices in the response are scoped to the `Landlord` client type.
- `penalties_gross` excludes penalties with status `waived` or `pending`.
- `debt` reflects only records in `pending` status from the debtors table.


---

<!-- doc: customer/debtors url: https://valora.spotahome.com/developers/customer/debtors -->

# Debt Records

List outstanding and historical debt records for the authenticated customer account.

Shared conventions and base URLs live in [Customer API Documentation](./introduction).

> **API hosts:** 
>
> - Production `https://valora.spotahome.com/` (sandbox compatible); 
>
> - Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible).

**Endpoint (production):** `GET https://valora.spotahome.com/api/v1/c/debtors`
**Endpoint (sandbox):** `GET https://valora-testing.laravel.cloud/api/sandbox/v1/c/debtors`

---

## Authentication

| Requirement | Details |
| --- | --- |
| **Type** | Bearer token (long-lived API token, `auth:api` guard) |
| **How to obtain a token** | See [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) |
| **Header** | `Authorization: Bearer {token}` |
| **Accept** | `Accept: application/json` |
| **Token class** | Customer only. Non-customer users receive HTTP 403 with message `Access denied. This resource is restricted to customers.` from `CheckCustomerApi` middleware. |
| **Rate limit** | 120 requests per minute per authenticated customer. |

---

## Connection test

```http
GET https://valora.spotahome.com/api/v1/c/debtors?test=connection
Authorization: Bearer {token}
Accept: application/json
```

**Response:** `200 OK`

```json
{
  "message": "You are connected!",
  "accepted_params": {
    "test": "Set to \"connection\" to test API connection",
    "search": "string, optional (booking reference or invoice number)",
    "status": "string, optional (pending|overdue|paid|settled|written_off|voided)",
    "debt_type": "string, optional (invoices_not_paid|transfers_badly_made|security_deposit_conflict|other)",
    "reference_id": "string, optional (exact booking or reference ID)",
    "due_date_from": "date (YYYY-MM-DD), optional",
    "due_date_to": "date (YYYY-MM-DD), optional",
    "amount_from": "decimal, optional (e.g., 100.50)",
    "amount_to": "decimal, optional (e.g., 500.00)",
    "page": "integer, optional (page number, default = 1)",
    "per_page": "integer, optional (items per page, default = 100, max = 500)"
  }
}
```

---

## Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `test` | string | No | Set to `connection` to perform a connection test. |
| `search` | string | No | Search in `reference_id` and `invoice_number`. |
| `status` | string | No | Filter by debt status. See [Debt statuses](#debt-statuses). |
| `debt_type` | string | No | Filter by debt type. See [Debt types](#debt-types). |
| `reference_id` | string | No | Exact booking or reference ID. |
| `due_date_from` | date (YYYY-MM-DD) | No | Include records with due date on or after this date. |
| `due_date_to` | date (YYYY-MM-DD) | No | Include records with due date on or before this date. |
| `amount_from` | number | No | Minimum amount in major units (e.g. `100.50`). Stored internally in minor units. |
| `amount_to` | number | No | Maximum amount in major units. |
| `page` | integer | No | Page number. Default: `1`. |
| `per_page` | integer | No | Items per page. Default: `100`, max: `500`. |

---

## Success response

**Status:** `200 OK`

```json
{
  "message": "Debt records retrieved successfully.",
  "pagination": {
    "total": 2,
    "per_page": 100,
    "current_page": 1,
    "last_page": 1,
    "from": 1,
    "to": 2
  },
  "data": [
    {
      "id": "uuid-debt-123",
      "status": "pending",
      "reference_id": "BK123456",
      "amount": 50000,
      "currency": "EUR",
      "debt_type": "invoices_not_paid",
      "invoice_number": "INV-2025-001",
      "due_date": "2025-03-01",
      "payment_method": null,
      "settled_from": null,
      "original_amount": 50000,
      "final_amount": null,
      "paid_at": null,
      "written_off_at": null,
      "settled_on": null,
      "voided_at": null
    }
  ]
}
```

### Debt record object

| Field | Type | Description |
|-------|------|-------------|
| `id` | string (UUID) | Debt record unique identifier. |
| `status` | string | Current status. See [Debt statuses](#debt-statuses). |
| `reference_id` | string | Booking or reference identifier this debt is linked to. |
| `amount` | number | Current amount in minor units (cents). |
| `currency` | string | Currency code. |
| `debt_type` | string | Category of debt. See [Debt types](#debt-types). |
| `invoice_number` | string \| null | Related invoice number, if applicable. |
| `due_date` | date \| null | Payment due date. |
| `payment_method` | string \| null | Payment method used when settled (e.g. `transfer`, `credit_card`). |
| `settled_from` | string \| null | Source reference when settled via a booking. |
| `original_amount` | number \| null | Original debt amount before any adjustments. |
| `final_amount` | number \| null | Final settled amount, if different from `original_amount`. |
| `paid_at` | string \| null | ISO 8601 timestamp when marked paid. |
| `written_off_at` | string \| null | ISO 8601 timestamp when written off. |
| `settled_on` | date \| null | Date settled. |
| `voided_at` | string \| null | ISO 8601 timestamp when voided. |

---

## Debt statuses

| Status | Description |
|--------|-------------|
| `pending` | Debt is open and awaiting payment. |
| `overdue` | Debt is past its due date. |
| `paid` | Debt has been paid in full. |
| `settled` | Debt has been settled (e.g. via a booking offset). |
| `written_off` | Debt has been written off. |
| `voided` | Debt record has been voided. |

---

## Debt types

| Type | Description |
|------|-------------|
| `invoices_not_paid` | Invoice amount that was not collected. |
| `transfers_badly_made` | Erroneous transfer that created a debt. |
| `security_deposit_conflict` | Debt arising from a security deposit dispute. |
| `other` | Other debt type. |

---

## Errors

| Status | Meaning |
|--------|---------|
| `404 Not Found` | No debt records matched the filters. |

```json
{
  "message": "No debt records found for the given filters.",
  "data": []
}
```

---

## Notes

- `amount_from` and `amount_to` accept major units; all amounts in the response are in **minor units** (cents).
- Results are ordered by `due_date` descending (most recent first).
- Some internal fields may not be included in the response for security purpose.
- This endpoint shows all debt records regardless of status. Use `?status=pending` or `?status=overdue` to filter for actionable items only.


---

<!-- doc: customer/introduction url: https://valora.spotahome.com/developers/customer/introduction -->

# Customer API Documentation

Documentation for customer-scoped REST API endpoints. Each endpoint has its own file so the request/response contract stays focused and easy to maintain.

> **API hosts** (use these with the paths below):
>
> - Production: `https://valora.spotahome.com/` (sandbox compatible)
> - Staging / testing: `https://valora-testing.laravel.cloud/` (sandbox compatible)

## Shared conventions

| Convention | Details |
|------------|---------|
| **Auth type** | Bearer token (long-lived API token, `auth:api` guard) |
| **How to obtain a token** | See [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) |
| **Headers** | `Authorization: Bearer {token}` and `Accept: application/json` (both required) |
| **Token class** | Customer only. Non-customer users receive HTTP 403 from `CheckCustomerApi` middleware. |
| **Rate limit** | 120 requests per minute per authenticated customer |
| **Customer API (production)** | `https://valora.spotahome.com/api/v1/c` |
| **Customer API (sandbox)** | `https://valora-testing.laravel.cloud/api/sandbox/v1/c` |
| **Amounts** | All monetary values in responses are in **minor units** (cents). Filter parameters (`amount_from`, `amount_to`) accept major units (e.g. `100.50`). |
| **Pagination** | Default page size: `100`. Maximum: `500`. |
| **Connection test** | Send `?test=connection` (or no parameters where documented) to receive `200 You are connected!` with accepted params. |

Each customer endpoint page links back here for shared conventions, auth, and base URLs.

## Endpoints

| Resource | Method | Production URL | Description |
|----------|--------|------------------|-------------|
| [Transactions](transactions/transactions) | `GET` | `https://valora.spotahome.com/api/v1/c/transactions` | List completed transactions. |
| [Scheduled Transactions](transactions/scheduled-transactions) | `GET` | `https://valora.spotahome.com/api/v1/c/transactions/scheduled` | List upcoming/scheduled payments. |
| [Invoices](invoicing/invoices) | `GET` | `https://valora.spotahome.com/api/v1/c/invoices` | List invoices and credit notes. |
| [Penalties](penalties/penalties) | `GET` | `https://valora.spotahome.com/api/v1/c/penalties` | List penalty records. |
| [Debt](debtors/debtors) | `GET` | `https://valora.spotahome.com/api/v1/c/debtors` | List outstanding debt records. |
| [Conciliation by Booking](conciliations/conciliation-by-booking) | `GET` | `https://valora.spotahome.com/api/v1/c/conciliation/booking` | Full financial conciliation for a booking. |


---

<!-- doc: customer/invoices url: https://valora.spotahome.com/developers/customer/invoices -->

# Invoices

List invoices and credit notes for the authenticated customer account.

Shared conventions and base URLs live in [Customer API Documentation](./introduction).

> **API hosts:** Production `https://valora.spotahome.com/` (sandbox compatible); Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible).

**Endpoint (production):** `GET https://valora.spotahome.com/api/v1/c/invoices`
**Endpoint (sandbox):** `GET https://valora-testing.laravel.cloud/api/sandbox/v1/c/invoices`

---

## Authentication

| Requirement | Details |
| --- | --- |
| **Type** | Bearer token (long-lived API token, `auth:api` guard) |
| **How to obtain a token** | See [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) |
| **Header** | `Authorization: Bearer {token}` |
| **Accept** | `Accept: application/json` |
| **Token class** | Customer only. Non-customer users receive HTTP 403 with message `Access denied. This resource is restricted to customers.` from `CheckCustomerApi` middleware. |
| **Rate limit** | 120 requests per minute per authenticated customer. |

---

## Connection test

```http
GET https://valora.spotahome.com/api/v1/c/invoices?test=connection
Authorization: Bearer {token}
Accept: application/json
```

**Response:** `200 OK`

```json
{
  "message": "You are connected!",
  "accepted_params": {
    "test": "Set to \"connection\" to test API connection",
    "search": "string, optional (invoice holder, document type or original document)",
    "issue_date_from": "date (YYYY-MM-DD), optional",
    "issue_date_to": "date (YYYY-MM-DD), optional",
    "amount_from": "decimal, optional (e.g., 100.50)",
    "amount_to": "decimal, optional (e.g., 500.00)",
    "doc_type": "string, optional (exact document type)",
    "doc_number": "string, optional (exact document number)",
    "booking_id": "string, optional (exact booking reference)",
    "page": "integer, optional (page number, default = 1)",
    "per_page": "integer, optional (items per page, default = 100, max = 500)"
  }
}
```

---

## Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `test` | string | No | Set to `connection` to perform a connection test. |
| `search` | string | No | Search in `booking_id`, `document_id`, `doc_type`, `invoice_to`, `original_doc`. |
| `issue_date_from` | date (YYYY-MM-DD) | No | Include invoices issued on or after this date. |
| `issue_date_to` | date (YYYY-MM-DD) | No | Include invoices issued on or before this date. |
| `amount_from` | number | No | Minimum amount in major units (e.g. `100.50`). Matched against `amount_net` or `amount_gross`. |
| `amount_to` | number | No | Maximum amount in major units. Matched against `amount_net` or `amount_gross`. |
| `doc_type` | string | No | Exact document type (e.g. `Invoice`, `Credit note`). |
| `doc_number` | string | No | Exact document number (matched against the `document_id` field). |
| `booking_id` | string | No | Exact booking reference. |
| `page` | integer | No | Page number. Default: `1`. |
| `per_page` | integer | No | Items per page. Default: `100`, max: `500`. |

---

## Success response

**Status:** `200 OK`

```json
{
  "message": "Invoices retrieved successfully.",
  "pagination": {
    "total": 20,
    "per_page": 100,
    "current_page": 1,
    "last_page": 1,
    "from": 1,
    "to": 20
  },
  "data": [
    {
      "booking_id": "BK123456",
      "document_id": "INV-2025-001",
      "doc_type": "Invoice",
      "invoice_to": "John Smith",
      "issue_date": "2025-10-01",
      "amount_net": 100000,
      "amount_gross": 121000,
      "currency": "EUR"
    }
  ]
}
```

### Invoice object

| Field | Type | Description |
|-------|------|-------------|
| `booking_id` | string | Booking reference. |
| `document_id` | string | Document identifier. |
| `doc_type` | string | Document type (`Invoice` or `Credit note`). |
| `invoice_to` | string | Invoiced party name. |
| `original_doc` | string \| null | Reference to the original document (credit notes only). |
| `issue_date` | date | Issue date. |
| `amount_net` | integer | Net amount in minor units (cents). |
| `amount_gross` | integer | Gross amount including taxes, in minor units (cents). |
| `currency` | string | Currency code. |

### No results response

**Status:** `404 Not Found`

### Example response

```json
{
  "message": "No invoices found for the given filters.",
  "pagination": {
    "total": 0,
    "per_page": 100,
    "current_page": 1,
    "last_page": 1,
    "from": null,
    "to": null
  },
  "data": []
}
```

### Response notes

- `pagination.total` is `0` when no invoices match the filters.
- `data` is always an array.
- The `404` response still carries pagination metadata for consistent client handling.

---

## Errors

| Status | Meaning |
|--------|---------|
| `404 Not Found` | No invoices matched the filters. |

```json
{
  "message": "No invoices found for the given filters.",
  "data": []
}
```

---

## Notes

- `amount_from` and `amount_to` accept major units; all amounts in the response are in **minor units** (cents).
- Results are ordered by `issue_date` descending (most recent first).
- `doc_number` is the filter parameter name; the response field is `document_id`.


---

<!-- doc: customer/penalties url: https://valora.spotahome.com/developers/customer/penalties -->

# Penalties

List penalty records for the authenticated customer account.

Shared conventions and base URLs live in [Customer API Documentation](./introduction).

> **API hosts:** Production `https://valora.spotahome.com/` (sandbox compatible); Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible).

**Endpoint (production):** `GET https://valora.spotahome.com/api/v1/c/penalties`
**Endpoint (sandbox):** `GET https://valora-testing.laravel.cloud/api/sandbox/v1/c/penalties`

---

## Authentication

| Requirement | Details |
| --- | --- |
| **Type** | Bearer token (long-lived API token, `auth:api` guard) |
| **How to obtain a token** | See [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) |
| **Header** | `Authorization: Bearer {token}` |
| **Accept** | `Accept: application/json` |
| **Token class** | Customer only. Non-customer users receive HTTP 403 with message `Access denied. This resource is restricted to customers.` from `CheckCustomerApi` middleware. |
| **Rate limit** | 120 requests per minute per authenticated customer. |

---

## Connection test

```http
GET https://valora.spotahome.com/api/v1/c/penalties?test=connection
Authorization: Bearer {token}
Accept: application/json
```

**Response:** `200 OK`

```json
{
  "message": "You are connected!",
  "accepted_params": {
    "test": "Set to \"connection\" to test API connection",
    "search": "string, optional (booking reference or invoice number)",
    "status": "string, optional (pending|notify|paid|waived|settled)",
    "booking_id": "string, optional (exact booking reference)",
    "due_date_from": "date (YYYY-MM-DD), optional",
    "due_date_to": "date (YYYY-MM-DD), optional",
    "amount_from": "decimal, optional (e.g., 100.50)",
    "amount_to": "decimal, optional (e.g., 500.00)",
    "page": "integer, optional (page number, default = 1)",
    "per_page": "integer, optional (items per page, default = 100, max = 500)"
  }
}
```

---

## Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `test` | string | No | Set to `connection` to perform a connection test. |
| `search` | string | No | Search in `booking_id` and `invoice_number`. |
| `status` | string | No | Filter by penalty status. See [Penalty statuses](#penalty-statuses). |
| `booking_id` | string | No | Exact booking reference. |
| `due_date_from` | date (YYYY-MM-DD) | No | Include penalties with due date on or after this date. |
| `due_date_to` | date (YYYY-MM-DD) | No | Include penalties with due date on or before this date. |
| `amount_from` | number | No | Minimum amount in major units (e.g. `100.50`). Stored internally in minor units. |
| `amount_to` | number | No | Maximum amount in major units. |
| `page` | integer | No | Page number. Default: `1`. |
| `per_page` | integer | No | Items per page. Default: `100`, max: `500`. |

---

## Success response

**Status:** `200 OK`

```json
{
  "message": "Penalties retrieved successfully.",
  "pagination": {
    "total": 3,
    "per_page": 100,
    "current_page": 1,
    "last_page": 1,
    "from": 1,
    "to": 3
  },
  "data": [
    {
      "id": "uuid-penalty-123",
      "booking_id": "BK123456",
      "status": "notify",
      "invoice_number": "PEN-2025-001",
      "amount": 3000,
      "amount_eur": 3000,
      "rate_to_eur": "1.0000000000",
      "currency": "EUR",
      "due_date": "2025-02-01",
      "cancelled_on": null,
      "link_view": "https://...",
      "link_download": "https://...",
      "settled_booking": null,
      "original_booking_amount": null,
      "final_amount": null,
      "timestamp_notify": "2025-01-20T09:00:00.000000Z",
      "timestamp_paid": null,
      "timestamp_waived": null,
      "timestamp_settled": null
    }
  ]
}
```

### Penalty object

| Field | Type | Description |
|-------|------|-------------|
| `id` | string (UUID) | Penalty unique identifier. |
| `booking_id` | string | Booking reference. |
| `status` | string | Current status. See [Penalty statuses](#penalty-statuses). |
| `invoice_number` | string \| null | Penalty invoice number. |
| `amount` | integer | Penalty amount in minor units (cents). |
| `amount_eur` | integer \| null | Amount converted to EUR in minor units. |
| `rate_to_eur` | string \| null | Exchange rate used for EUR conversion. |
| `currency` | string | Original currency code. |
| `due_date` | date \| null | Payment due date. |
| `cancelled_on` | date \| null | Date cancelled, if applicable. |
| `link_view` | string \| null | URL to view the penalty document. |
| `link_download` | string \| null | URL to download the penalty document. |
| `settled_booking` | string \| null | Reference of the booking used to settle this penalty. |
| `original_booking_amount` | integer \| null | Original booking amount at time of penalty (minor units). |
| `final_amount` | integer \| null | Final settled amount in minor units. |
| `timestamp_notify` | string \| null | ISO 8601 timestamp when notification was sent. |
| `timestamp_paid` | string \| null | ISO 8601 timestamp when marked paid. |
| `timestamp_waived` | string \| null | ISO 8601 timestamp when waived. |
| `timestamp_settled` | string \| null | ISO 8601 timestamp when settled. |

---

## Penalty statuses

| Status | Description |
|--------|-------------|
| `pending` | Penalty created; not yet notified. |
| `notify` | Notification sent to the customer. |
| `paid` | Penalty has been paid. |
| `waived` | Penalty has been waived. |
| `settled` | Penalty settled via an alternative arrangement. |

---

## Errors

| Status | Meaning |
|--------|---------|
| `404 Not Found` | No penalties matched the filters. |

```json
{
  "message": "No penalties found for the given filters.",
  "data": []
}
```

---

## Notes

- `amount_from` and `amount_to` accept major units; all amounts in the response are in **minor units** (cents).
- Results are ordered by `due_date` descending (most recent first).
- Internal fields (`account_id`, `account_manager`, `client_tin`, `landlord_name`, etc.) are not included in the response.


---

<!-- doc: customer/scheduled-transactions url: https://valora.spotahome.com/developers/customer/scheduled-transactions -->

# Scheduled Transactions

List upcoming (scheduled) payments for the authenticated customer account.

Shared conventions and base URLs live in [Customer API Documentation](./introduction).

> **API hosts:** Production `https://valora.spotahome.com/` (sandbox compatible); Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible).

**Endpoint (production):** `GET https://valora.spotahome.com/api/v1/c/transactions/scheduled`
**Endpoint (sandbox):** `GET https://valora-testing.laravel.cloud/api/sandbox/v1/c/transactions/scheduled`

---

## Authentication

| Requirement | Details |
| --- | --- |
| **Type** | Bearer token (long-lived API token, `auth:api` guard) |
| **How to obtain a token** | See [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) |
| **Header** | `Authorization: Bearer {token}` |
| **Accept** | `Accept: application/json` |
| **Token class** | Customer only. Non-customer users receive HTTP 403 with message `Access denied. This resource is restricted to customers.` from `CheckCustomerApi` middleware. |
| **Rate limit** | 120 requests per minute per authenticated customer. |

---

## Connection test

```http
GET https://valora.spotahome.com/api/v1/c/transactions/scheduled?test=connection
Authorization: Bearer {token}
Accept: application/json
```

**Response:** `200 OK`

```json
{
  "message": "You are connected!",
  "accepted_params": {
    "test": "Set to \"connection\" to test API connection",
    "booking_id": "string, optional (exact booking reference)",
    "status": "string, optional (upcoming|delayed|blocked|missing_iban|missing_details|has_debt|blocked_missing_details|blocked_missing_iban|blocked_account_debtor)",
    "currency": "string, optional (e.g., EUR, USD)",
    "due_date_from": "date (YYYY-MM-DD), optional",
    "due_date_to": "date (YYYY-MM-DD), optional",
    "amount_from": "decimal, optional (e.g., 100.50)",
    "amount_to": "decimal, optional (e.g., 500.00)",
    "search": "string, optional (booking reference, account name or IBAN)",
    "page": "integer, optional (page number, default = 1)",
    "per_page": "integer, optional (items per page, default = 100, max = 500)"
  }
}
```

---

## Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `test` | string | No | Set to `connection` to perform a connection test. |
| `booking_id` | string | No | Exact booking reference. |
| `status` | string | No | Filter by payment status. See [Payment statuses](#payment-statuses). |
| `currency` | string | No | Currency code (e.g. `EUR`, `USD`). |
| `due_date_from` | date (YYYY-MM-DD) | No | Due date from (inclusive). |
| `due_date_to` | date (YYYY-MM-DD) | No | Due date to (inclusive). |
| `amount_from` | number | No | Minimum amount in major units (e.g. `100.50`). Stored internally in minor units. |
| `amount_to` | number | No | Maximum amount in major units. |
| `search` | string | No | Search in `booking_id`, `account_name`, `iban`. |
| `page` | integer | No | Page number. Default: `1`. |
| `per_page` | integer | No | Items per page. Default: `100`, max: `500`. |

---

## Success response

**Status:** `200 OK`

```json
{
  "message": "Scheduled transactions retrieved successfully.",
  "pagination": {
    "total": 5,
    "per_page": 100,
    "current_page": 1,
    "last_page": 1,
    "from": 1,
    "to": 5
  },
  "data": [
    {
      "booking_id": "BK123456",
      "amount": 120000,
      "currency": "EUR",
      "iban": "ES1234567890123456789012",
      "due_date": "2025-02-15",
      "due_date_erp": "2025-02-15",
      "status": "upcoming",
      "account_name": "John Doe",
      "b2b": false,
      "transfer_blocked": false,
      "available_bank_account": true,
      "same_account": false,
      "days_delayed": 0,
      "has_debt": false,
      "updated_at": "2025-01-15T10:30:00.000000Z"
    }
  ]
}
```

### Scheduled transaction object

| Field | Type | Description |
|-------|------|-------------|
| `booking_id` | string | Booking reference. |
| `amount` | integer | Amount in minor units (cents). |
| `currency` | string | Currency code. |
| `iban` | string | Destination IBAN. |
| `due_date` | date | Scheduled due date. |
| `due_date_erp` | date | Due date as provided by the ERP. |
| `status` | string | Payment status. See [Payment statuses](#payment-statuses). |
| `account_name` | string | Account holder name. |
| `b2b` | boolean | Whether this is a business-to-business payment. |
| `transfer_blocked` | boolean | Whether the transfer is currently blocked. |
| `available_bank_account` | boolean | Whether a valid bank account is on file. |
| `same_account` | boolean | Whether origin and destination are the same account. |
| `days_delayed` | integer | Number of days past the due date (0 when not delayed). |
| `has_debt` | boolean | Whether the account has an outstanding debt. |
| `updated_at` | string (ISO 8601) | Last update timestamp. |

### No results response

**Status:** `404 Not Found`

### Example response

```json
{
  "message": "No scheduled transactions found for the given filters.",
  "pagination": {
    "total": 0,
    "per_page": 100,
    "current_page": 1,
    "last_page": 1,
    "from": null,
    "to": null
  },
  "data": []
}
```

### Response notes

- `pagination.total` is `0` when the filters match no scheduled payments.
- `data` is always an array.
- Consumers can treat `404` as "no matching rows" without a schema change.

---

## Payment statuses

| Status | Description |
|--------|-------------|
| `upcoming` | Scheduled and ready — no issues detected. |
| `delayed` | Due date has passed but no blocking issue detected. |
| `blocked` | Stopped by an external service. |
| `missing_iban` | No valid IBAN on file for this booking. |
| `missing_details` | One or more required details are missing. |
| `has_debt` | Account has an outstanding debt; payment may be held. |
| `blocked_missing_details` | Blocked and missing required details. |
| `blocked_missing_iban` | Blocked because no valid IBAN is on file. |
| `blocked_account_debtor` | Blocked because the account has a debt. |

---

## Errors

| Status | Meaning |
|--------|---------|
| `404 Not Found` | No scheduled transactions matched the filters. |

```json
{
  "message": "No scheduled transactions found for the given filters.",
  "data": []
}
```

---

## Notes

- `amount_from` and `amount_to` accept major units; all amounts in the response are in **minor units** (cents).
- Results are ordered by `due_date` ascending (soonest first).
- Sensitive employee fields (`account_manager`, `comment`, `tenant_email`, `missing_details`, etc.) are not included in the response.


---

<!-- doc: customer/transactions url: https://valora.spotahome.com/developers/customer/transactions -->

# Transactions

List completed transactions for the authenticated customer account.

Shared conventions and base URLs live in [Customer API Documentation](./introduction).

> **API hosts:** Production `https://valora.spotahome.com/` (sandbox compatible); Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible).

**Endpoint (production):** `GET https://valora.spotahome.com/api/v1/c/transactions`
**Endpoint (sandbox):** `GET https://valora-testing.laravel.cloud/api/sandbox/v1/c/transactions`

---

## Authentication

| Requirement | Details |
| --- | --- |
| **Type** | Bearer token (long-lived API token, `auth:api` guard) |
| **How to obtain a token** | See [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) |
| **Header** | `Authorization: Bearer {token}` |
| **Accept** | `Accept: application/json` |
| **Token class** | Customer only. Non-customer users receive HTTP 403 with message `Access denied. This resource is restricted to customers.` from `CheckCustomerApi` middleware. |
| **Rate limit** | 120 requests per minute per authenticated customer. |

---

## Connection test

```http
GET https://valora.spotahome.com/api/v1/c/transactions?test=connection
Authorization: Bearer {token}
Accept: application/json
```

**Response:** `200 OK`

```json
{
  "message": "You are connected!",
  "accepted_params": {
    "test": "Set to \"connection\" to test API connection",
    "search": "string, optional (beneficiary, type or bank account last 4 digits)",
    "pay_date_from": "date (YYYY-MM-DD), optional",
    "pay_date_to": "date (YYYY-MM-DD), optional",
    "amount_from": "decimal, optional (e.g., 100.50)",
    "amount_to": "decimal, optional (e.g., 500.00)",
    "booking_id": "string, optional (exact booking reference)",
    "page": "integer, optional (page number, default = 1)",
    "per_page": "integer, optional (items per page, default = 100, max = 500)"
  }
}
```

---

## Query parameters

| Parameter | Type | Required | Description                                                                          |
|-----------|------|----------|--------------------------------------------------------------------------------------|
| `test` | string | No | Set to `connection` to perform a connection test.                                    |
| `search` | string | No | Search in `beneficiary`, `bank_account`, `type`.                                     |
| `pay_date_from` | date (YYYY-MM-DD) | No | Include transactions issued on or after this date.                                   |
| `pay_date_to` | date (YYYY-MM-DD) | No | Include transactions issued on or before this date.                                  |
| `amount_from` | number | No | Minimum amount in major units (e.g. `100.50`). Stored internally in minor units (cents). |
| `amount_to` | number | No | Maximum amount in major units.                                                       |
| `booking_id` | string | No | Exact booking reference.                                                             |
| `page` | integer | No | Page number. Default: `1`.                                                           |
| `per_page` | integer | No | Items per page. Default: `100`, max: `500`.                                          |

---

## Success response

**Status:** `200 OK`

```json
{
  "message": "Transactions retrieved successfully.",
  "pagination": {
    "total": 123,
    "per_page": 100,
    "current_page": 1,
    "last_page": 2,
    "from": 1,
    "to": 100
  },
  "data": [
    {
      "booking_id": "BK123456",
      "beneficiary": "John Smith",
      "type": "Transfer",
      "action": "paid",
      "bank_account": "ES9121000418450200051332",
      "amount": 100000,
      "issued_on": "2025-10-01",
      "currency": "EUR"
    }
  ]
}
```

### Transaction object

| Field | Type | Description |
|-------|------|-------------|
| `booking_id` | string | Booking reference. |
| `beneficiary` | string | Recipient name. |
| `type` | string | Transaction type. |
| `action` | string | Action performed. |
| `bank_account` | string | Destination bank account / IBAN. |
| `amount` | integer | Amount in minor units (cents). |
| `issued_on` | date | Payment date. |
| `currency` | string | Currency code. |

### No results response

**Status:** `404 Not Found`

### Example response

```json
{
  "message": "No transactions found for the given filters.",
  "pagination": {
    "total": 0,
    "per_page": 100,
    "current_page": 1,
    "last_page": 1,
    "from": null,
    "to": null
  },
  "data": []
}
```

### Response notes

- `pagination.total` is `0` when no rows match the filters.
- `data` is always an array.
- The `404` response still includes pagination so clients can keep the same rendering path.

---

## Errors

| Status | Meaning |
|--------|---------|
| `404 Not Found` | No transactions matched the filters. |

```json
{
  "message": "No transactions found for the given filters.",
  "data": []
}
```

---

## Notes

- `amount_from` and `amount_to` accept major units; all amounts in the response are in **minor units** (cents).
- Results are ordered by `issued_on` descending (most recent first).


---

<!-- doc: integrations/introduction url: https://valora.spotahome.com/developers/integrations/introduction -->

# Integrations Documentation

Step-by-step guides for connecting external tools and automation platforms — n8n, Zapier, Make, Apps Script, and others — to the Valora API. Each page is task-oriented (built around a single workflow) rather than endpoint-oriented, so you can follow it end-to-end without jumping between docs.

> **API hosts** (use these with the paths below):
>
> - Production: `https://valora.spotahome.com/` (sandbox compatible)
> - Staging / testing: `https://valora-testing.laravel.cloud/` (sandbox compatible)

## Before you start

Most integrations call **authenticated** Valora endpoints, so you'll usually need:

| Requirement | Details |
|------------|---------|
| **Auth type** | Bearer token (long-lived API token, `auth:api` guard) |
| **How to obtain a token** | See [Long-Lived API Token Authentication](/developers/auth/api-key-authentication) |
| **Headers** | `Authorization: Bearer {token}` and `Accept: application/json` |
| **Token class** | Match the endpoint — Employee tokens for `/api/v1/e/...`, Customer tokens for `/api/v1/c/...`. The wrong class returns HTTP 403. |
| **Sandbox first** | Test against `https://valora-testing.laravel.cloud/api/sandbox/v1/...` with a sandbox token before pointing the workflow at production. |
| **Rate limits** | 5000 req/min per employee, 120 req/min per customer, 60 req/min per IP for guest (unauthenticated) routes. |

The [Get Started](/developers/get-started/introduction) section covers the API conventions (headers, pagination, errors, amounts in minor units) that every integration relies on — read it first if you haven't already.

## Guides

| Guide | Tool | Workflow |
|-------|------|----------|
| [Send a File to Valora Storage](n8n-send-file-to-storage) | n8n | Configure an n8n HTTP Request node to upload a generated file (XML, PDF, CSV, …) to Valora's storage API. |

> More step-by-step guides for n8n and other automation tools (Zapier, Make, Apps Script) will be added here as they're written. If you're integrating with a tool not yet documented, the [authentication reference](/developers/auth/api-key-authentication) plus the relevant [Customer](/developers/customer/introduction) or [Employee](/developers/employee/introduction) endpoint page is enough to wire up almost any HTTP-capable platform.

> **Looking for unauthenticated endpoints?** Public routes under `/api/v1/open/...` (VAT validation, etc.) live in their own [Open Endpoints](/developers/open/introduction) section.


---

<!-- doc: integrations/n8n-send-file-to-storage url: https://valora.spotahome.com/developers/integrations/n8n-send-file-to-storage -->

# n8n Workflow: Send File to Valora Storage

This guide explains how to configure an n8n workflow to send a file to the Valora storage API endpoint. This is useful when you've already generated a file in n8n (e.g., XML, PDF, CSV) and need to store it in Valora's document system.

---

## Prerequisites

- n8n workflow with a file already created/saved
- Valora Employee API Token
- Access to Valora API (production or sandbox)
- Your Valora API base URL (e.g., `https://valora.spotahome.com`)

---

## Quick Reference: API Endpoints

| Environment | URL |
|------------|-----|
| **Production** | `https://valora.spotahome.com/api/v1/storage/file` |
| **Sandbox** | `https://valora-testing.laravel.cloud/api/sandbox/v1/storage/file` |

---

## Step by Step: Using n8n UI

This section provides detailed instructions for configuring the upload using n8n's visual interface. No coding required.

### Step 1: Add HTTP Request Node

1. In your n8n workflow, click the **+** button to add a new node
2. Search for **"HTTP Request"** and select it
3. Connect it after the node that creates/saves your file
4. Rename the node to something descriptive like **"Upload to Valora Storage"**

### Step 2: Configure Basic Settings

1. Click on the HTTP Request node to open its settings
2. In the **Method** dropdown, select **POST**
3. In the **URL** field, enter:
   - **Production:** `https://valora.spotahome.com/api/v1/storage/file`
   - **Sandbox:** `https://valora-testing.laravel.cloud/api/sandbox/v1/storage/file`

### Step 3: Set Up Authentication

1. Scroll down to the **Authentication** section
2. Click the dropdown and select **"Header Auth"**
3. In the **Name** field, enter: `Authorization`
4. In the **Value** field, enter: `Bearer YOUR_API_TOKEN`
   - Replace `YOUR_API_TOKEN` with your actual Valora Employee API token

> **Tip:** For better security, consider using n8n credentials instead of hardcoding the token. Go to **Credentials** → **Create New** → **Header Auth** and save your token there.

### Step 4: Add Request Headers

1. Expand the **Options** section (click "Show more options" if needed)
2. Find the **Headers** section
3. Click **"Add Header"**
4. Set:
   - **Name:** `Accept`
   - **Value:** `application/json`

> **Important:** Do NOT add a `Content-Type` header. n8n will automatically set this to `multipart/form-data` when you configure the body.

### Step 5: Configure Request Body

1. Scroll to the **Body** section
2. In the **Body Content Type** dropdown, select **"Form-Data"** or **"Multipart-Form-Data"**
3. You'll see a table to add parameters. Add the following fields:

#### Required Fields

| Name | Type | Value |
|------|------|-------|
| `file` | **File** | Click the expression icon (fx) and enter: `{{ $binary.data }}` |
| `file_name` | **String** | Enter your desired filename, e.g., `dac7-report-2025.xml` |
| `store_in` | **String** | Enter one of: `documents`, `invoicing`, `reporting`, or `reports` |

#### Optional Fields

| Name | Type | Value |
|------|------|-------|
| `category` | **String** | e.g., `dac7-reporting`, `invoice`, `report` |
| `description` | **String** | Description of the document |
| `visibility` | **String** | `public` or `private` |

### Step 6: Configure File Field (Important!)

The `file` field needs special attention:

1. Click on the **Value** field for the `file` parameter
2. Click the **expression icon** (fx) to enable expressions
3. Enter one of these based on your setup:

   **If your previous node outputs binary data:**
   ```
   {{ $binary.data }}
   ```

   **If you have a file path from previous node:**
   ```
   {{ $json.filePath }}
   ```

   **If you need to read a file from disk:**
   - First, add a **"Read Binary File"** node before the HTTP Request
   - Then use: `{{ $binary.data }}`

### Step 7: Test the Configuration

1. Click **"Execute Node"** to test
2. Check the output:
   - **Success:** You'll see a response with `"success": true` and a `document_id`
   - **Error:** Check the error message and refer to the troubleshooting section

### Step 8: Add Error Handling (Recommended)

1. Add an **IF** node after the HTTP Request node
2. Configure the condition:
   - **Value 1:** `{{ $json.success }}`
   - **Operation:** `Equal`
   - **Value 2:** `true`
3. Connect success path to continue your workflow
4. Connect error path to a notification node (e.g., email, Slack)

---

## Step by Step: For Advanced Developers

This section provides code-based examples and advanced configurations for developers comfortable with n8n expressions and JavaScript.

### Method 1: Using Code Node to Prepare Request

If you need to dynamically prepare the file and metadata before uploading:

```javascript
// Code Node: Prepare Upload Data
const fileName = `dac7-report-${$now.toFormat('yyyy-MM-dd')}.xml`;
const storeIn = $json.reportType === 'dac7' ? 'reporting' : 'documents';

return [{
  json: {
    fileName: fileName,
    storeIn: storeIn,
    category: 'dac7-reporting',
    description: `DAC7 report generated on ${$now.toFormat('yyyy-MM-dd HH:mm:ss')}`,
    visibility: 'private'
  },
  binary: {
    data: {
      data: Buffer.from($json.xmlContent, 'utf8'),
      mimeType: 'application/xml',
      fileName: fileName
    }
  }
}];
```

Then in HTTP Request node, use expressions:
- `file`: `{{ $binary.data }}`
- `file_name`: `{{ $json.fileName }}`
- `store_in`: `{{ $json.storeIn }}`
- `category`: `{{ $json.category }}`

### Method 2: Direct HTTP Request with Expressions

Configure the HTTP Request node with dynamic expressions:

**URL:**
```javascript
{{ ($env.ENVIRONMENT === 'sandbox' ? 'https://valora-testing.laravel.cloud/api/sandbox' : 'https://valora.spotahome.com/api') + '/v1/storage/file' }}
```

**Body Parameters:**

| Name | Expression |
|------|-----------|
| `file` | `{{ $binary.data }}` |
| `file_name` | `{{ $json.fileName || 'document-' + $now.toISO() + '.xml' }}` |
| `store_in` | `{{ $json.storeIn || 'documents' }}` |
| `category` | `{{ $json.category || 'api-upload' }}` |
| `description` | `{{ $json.description || 'Uploaded via n8n on ' + $now.toFormat('yyyy-MM-dd') }}` |
| `visibility` | `{{ $json.visibility || 'private' }}` |

### Method 3: Error Handling with Code Node

Add a Code node after HTTP Request to handle errors gracefully:

```javascript
// Code Node: Handle Upload Response
const response = $input.item.json;

if (response.success) {
  return [{
    json: {
      success: true,
      documentId: response.data.document_id,
      fileName: response.data.file_name,
      sizeBytes: response.data.size_bytes,
      storedIn: response.data.stored_in,
      message: 'File uploaded successfully'
    }
  }];
} else {
  // Log error and throw for error workflow path
  console.error('Upload failed:', response);
  throw new Error(response.message || 'Upload failed');
}
```

### Method 4: Complete Workflow with Validation

```javascript
// Code Node 1: Validate and Prepare
const fileSize = $binary.data.data.length;
const maxSize = 10 * 1024 * 1024; // 10 MB

if (fileSize > maxSize) {
  throw new Error(`File size (${fileSize} bytes) exceeds maximum (${maxSize} bytes)`);
}

const fileName = $json.fileName || `upload-${$now.toFormat('yyyy-MM-dd-HHmmss')}.${$json.fileExtension || 'xml'}`;
const storeIn = ['documents', 'invoicing', 'reporting', 'reports'].includes($json.storeIn) 
  ? $json.storeIn 
  : 'documents';

return [{
  json: {
    fileName,
    storeIn,
    category: $json.category || 'api-upload',
    description: $json.description || null,
    visibility: ['public', 'private'].includes($json.visibility) ? $json.visibility : 'private',
    fileSize
  },
  binary: {
    data: $binary.data
  }
}];
```

### Method 5: Batch Upload Multiple Files

If you need to upload multiple files:

```javascript
// Code Node: Process Multiple Files
const files = $input.all();

return files.map(item => ({
  json: {
    fileName: item.json.fileName,
    storeIn: item.json.storeIn || 'documents',
    category: item.json.category || 'batch-upload',
    index: item.json.index
  },
  binary: {
    data: item.binary.data
  }
}));
```

Then use **Split In Batches** node before HTTP Request to process one at a time, or use **HTTP Request** with "Process all items" enabled.

---

## Storage Destinations Reference

| `store_in` Value | Description | Default Visibility | Use Case |
|-----------------|-------------|-------------------|----------|
| `documents` | General document storage | `private` | Any document type |
| `invoicing` | Invoice documents | `public` | Invoices, billing documents |
| `reporting` | Reporting documents (DAC7, etc.) | `private` | XML reports, compliance documents |
| `reports` | General reports (CSV, ZIP, etc.) | `private` | Data exports, analytics |

---

## Response Format

### Success Response (201 Created)

```json
{
  "success": true,
  "message": "File uploaded successfully.",
  "data": {
    "document_id": "39f6b0c8-8997-46f1-9c9d-e9f765f89447",
    "file_name": "dac7-report-2025.xml",
    "size_bytes": 120431,
    "stored_in": "reporting"
  }
}
```

### Error Responses

**422 Validation Error:**
```json
{
  "success": false,
  "message": "Validation failed.",
  "errors": {
    "store_in": ["The selected store in is invalid."]
  }
}
```

**500 Server Error:**
```json
{
  "success": false,
  "message": "An unexpected error occurred while uploading the file.",
  "error": "Detailed error message (only in debug mode)"
}
```

**401 Unauthorized:**
```json
{
  "message": "Unauthenticated."
}
```

---

## Common Issues & Solutions

### Issue: "No file was uploaded"

**Symptoms:** API returns error about missing file

**Solutions:**
- Verify the file field uses `{{ $binary.data }}` expression
- Check that the previous node outputs binary data
- If using file path, add a **Read Binary File** node first
- Ensure the file parameter type is set to **File** (not String)

### Issue: "Validation failed"

**Symptoms:** 422 error with validation details

**Solutions:**
- Check that `file_name` is provided and is a string
- Verify `store_in` is exactly one of: `documents`, `invoicing`, `reporting`, `reports`
- Ensure file size is under 10 MB (10240 KB)
- Check that all required fields are present

### Issue: "Unauthenticated"

**Symptoms:** 401 error

**Solutions:**
- Verify your API token is valid and not expired
- Check that the token has employee permissions (not customer token)
- Ensure Authorization header format is: `Bearer YOUR_TOKEN` (with space after Bearer)
- Test the token with a simple API call first

### Issue: File Not Found or Binary Data Missing

**Symptoms:** Error about missing binary data

**Solutions:**
- If previous node outputs JSON with file path, add **Read Binary File** node
- Verify the binary data exists: check `{{ $binary }}` in previous node output
- Ensure file path is absolute or relative to n8n's working directory
- For Code nodes, make sure you're returning binary data correctly

### Issue: Wrong Content-Type

**Symptoms:** API doesn't recognize the file

**Solutions:**
- Don't manually set `Content-Type` header - let n8n handle it
- Use **Form-Data** or **Multipart-Form-Data** body type
- Ensure file field is set as **File** type, not String

---

## n8n Expression Examples

### Dynamic File Name with Timestamp

```javascript
{{ 'dac7-report-' + $now.toFormat('yyyy-MM-dd') + '.xml' }}
```

### Conditional Storage Destination

```javascript
{{ $json.reportType === 'dac7' ? 'reporting' : 'documents' }}
```

### Extract Document ID from Response

```javascript
{{ $json.data.document_id }}
```

### File Size Check Before Upload

```javascript
{{ $binary.data.data.length < 10485760 ? 'OK' : 'TOO_LARGE' }}
```

### Generate Filename from Multiple Sources

```javascript
{{ ($json.prefix || 'document') + '-' + ($json.id || $now.toFormat('yyyyMMdd')) + '.' + ($json.extension || 'xml') }}
```

---

## Best Practices

1. **Error Handling:** Always add error handling nodes after the HTTP Request
   - Use IF node to check `{{ $json.success }}`
   - Log errors for debugging
   - Send notifications on failure

2. **Logging:** Log the `document_id` for future reference
   - Store in a database
   - Include in notifications
   - Save to workflow execution logs

3. **File Size Validation:** Check file size before uploading
   - Max size: 10 MB (10240 KB)
   - Add validation in Code node if needed

4. **Naming Convention:** Use descriptive, timestamped filenames
   - Include date/time: `report-2025-01-15-143022.xml`
   - Include source: `dac7-2025-Q1.xml`
   - Avoid special characters

5. **Metadata:** Include `category` and `description` for better organization
   - Helps with document retrieval
   - Improves audit trail
   - Aids in compliance

6. **Testing:** Test with sandbox environment first
   - Use sandbox endpoint for development
   - Verify file uploads correctly
   - Check document appears in system

7. **Security:** Use n8n credentials for API tokens
   - Don't hardcode tokens in workflows
   - Use credential management
   - Rotate tokens regularly

8. **Retry Logic:** Consider adding retry for transient failures
   - Use n8n's built-in retry on error
   - Or implement custom retry logic in Code node

---

## Complete Example Workflow

### Scenario: Generate DAC7 XML and Upload

**Workflow Structure:**
1. **Code Node** - Generate XML content
2. **Code Node** - Convert to binary
3. **HTTP Request** - Upload to Valora
4. **IF Node** - Check success
5. **Code Node** - Log result (success path)
6. **Code Node** - Handle error (error path)

**Node 1: Generate XML**
```javascript
const xmlContent = `<?xml version="1.0"?>
<report>
  <docRefId>ES2025-001</docRefId>
  <generatedAt>${$now.toISO()}</generatedAt>
  <!-- Your XML content -->
</report>`;

return [{
  json: {
    xmlContent,
    fileName: `dac7-${$now.toFormat('yyyy-MM-dd')}.xml`
  }
}];
```

**Node 2: Convert to Binary**
```javascript
return [{
  json: $input.item.json,
  binary: {
    data: {
      data: Buffer.from($input.item.json.xmlContent, 'utf8'),
      mimeType: 'application/xml',
      fileName: $input.item.json.fileName
    }
  }
}];
```

**Node 3: HTTP Request** (configure as shown in UI steps)

**Node 4: IF Node**
- Condition: `{{ $json.success === true }}`

**Node 5: Log Success**
```javascript
return [{
  json: {
    message: 'Upload successful',
    documentId: $input.item.json.data.document_id,
    timestamp: $now.toISO()
  }
}];
```

---

## Testing Checklist

Before deploying your workflow to production:

- [ ] Test with a small file (< 1 MB)
- [ ] Test with maximum size file (10 MB)
- [ ] Test with different file types (XML, PDF, CSV)
- [ ] Test all storage destinations (`documents`, `invoicing`, `reporting`, `reports`)
- [ ] Test error scenarios (invalid token, missing fields, oversized file)
- [ ] Verify document appears in Valora system
- [ ] Check document metadata is correct
- [ ] Test in sandbox environment first
- [ ] Verify error handling works correctly
- [ ] Test with actual production data (if safe)

---

© Valora n8n Integration Guide


---

<!-- doc: open/introduction url: https://valora.spotahome.com/developers/open/introduction -->

# Open API Documentation

Documentation for Valora's **public, unauthenticated** REST endpoints. These routes live under the `/api/v1/open/...` path prefix and are intended for use cases where a bearer token isn't practical — third-party widgets, embedded forms, public lookup tools, and lightweight integrations.

> **API hosts** (use these with the paths below):
>
> - Production: `https://valora.spotahome.com/` (sandbox compatible)
> - Staging / testing: `https://valora-testing.laravel.cloud/` (sandbox compatible)

## Shared conventions

| Convention | Details |
|------------|---------|
| **Auth type** | None. Routes under `/api/v1/open/...` are public — no bearer token, no API key. |
| **Headers** | `Accept: application/json` is required. Send `Content-Type: application/json` when posting a JSON body. |
| **Token class** | Not applicable. |
| **Rate limit** | Guest bucket: **60 requests per minute per IP**. |
| **Open API (production)** | `https://valora.spotahome.com/api/v1/open` |
| **Open API (sandbox)** | `https://valora-testing.laravel.cloud/api/sandbox/v1/open` |
| **CORS** | Open routes can be called from browsers; non-open routes generally cannot. |
| **Caching** | Some endpoints cache upstream lookups (e.g. VIES) to keep response times low and reduce load on third-party services — see each endpoint page for TTLs. |

Each open endpoint page links back here for shared conventions, base URLs, and rate-limit details.

## When to use an open endpoint

Use these only for data that is genuinely public or where the cost of abuse is bounded by the guest rate limit. If the call returns customer-specific or employee-specific data, you want an [authenticated endpoint](/developers/auth/introduction) instead — the open prefix is **not** a way to skip authentication.

Typical use cases:

- VAT-number validation in a public checkout or signup form.
- Lookups that mirror data already exposed by an upstream public service.
- Health/status probes used by uptime monitors.

## Endpoints

| Resource | Method | Production URL | Description |
|----------|--------|----------------|-------------|
| [VAT Validation — Status](vat-validation-api) | `GET` | `https://valora.spotahome.com/api/v1/open/vat-vies/status` | Validate a VAT number against the European Commission VIES service. |
| [VAT Validation — Calculate](vat-validation-api) | `POST` | `https://valora.spotahome.com/api/v1/open/vat-vies/calculate` | Validate a VAT number (optional) and calculate VAT amounts based on the input. |


---

<!-- doc: open/vat-validation-api url: https://valora.spotahome.com/developers/open/vat-validation-api -->

# VAT Validation API Documentation

## Overview

This API provides two main endpoints:
1. **VIES Status Check** - Validates a VAT number against the European Commission VIES service (no calculations)
2. **VAT Calculation** - Validates a VAT number (if provided) and calculates VAT amounts based on the given input

Both endpoints use the VIES service to validate VAT numbers. The system includes automatic caching (5-day cache) to improve performance and reduce API calls.

> **API hosts:** Production `https://valora.spotahome.com/` (sandbox compatible); Staging / testing `https://valora-testing.laravel.cloud/` (sandbox compatible). Examples below use the production host; the same paths work on the staging host.

## Authentication

| Requirement | Details |
| --- | --- |
| **Type** | None — public routes under `https://valora.spotahome.com/api/v1/open/...` (no bearer token). |
| **How to obtain a token** | Not applicable. |
| **Header** | `Accept: application/json` (required). |
| **Token class** | Not applicable. |
| **Rate limit** | Unauthenticated requests use the `api` middleware **guest** bucket: **60 requests per minute per IP** by default (`API_RATE_LIMIT_GUEST_PER_MINUTE`; see `config/valora.php` and `AppServiceProvider::configureRateLimiting`). |

---

## Endpoints

### 1. VIES Status Check

**URL:** `GET https://valora.spotahome.com/api/v1/open/vat-vies/status`
**Method:** `GET`  

**Headers Required:**
* `Accept: application/json`

**Query Parameters:**

| Parameter        | Type   | Required | Description                                                      |
| ---------------- | ------ | -------- | ---------------------------------------------------------------- |
| `vat_number`     | string | Yes      | VAT number to validate via VIES (can include country prefix, e.g., "DE12345678" or just "12345678") |
| `vat_country`    | string | Yes      | 2-letter ISO country code of the VAT number (used if VAT number doesn't have a prefix) |
| `client_country` | string | Yes      | 2-letter ISO country code of the client requesting validation   |

**Example Request:**
```
GET https://valora.spotahome.com/api/v1/open/vat-vies/status?vat_number=12345678&vat_country=FR&client_country=ES
```

**Note:** If a `POST` request is sent to this endpoint, it will return a **400 Bad Request** with the message: "POST method not supported for calculations. Perhaps you meant to use GET"

### 2. VAT Calculation

**URL:** `https://valora.spotahome.com/api/v1/open/vat-vies/calculate`
**Methods:**
* `GET` → Connection check (no parameters allowed)
* `POST` → VAT validation and calculation (requires parameters)

**Headers Required:**
* `Accept: application/json`

---

## Request Parameters

### POST `https://valora.spotahome.com/api/v1/open/vat-vies/calculate`

You must send parameters in a `POST` request. They can be included as query string parameters or as a JSON body (recommended).

| Parameter        | Type    | Required | Description                                                      |
| ---------------- | ------- | -------- | ---------------------------------------------------------------- |
| `vat_amount`     | integer | Yes      | VAT base amount in **cents**. Example: `2999` for €29.99         |
| `vat_rate`       | integer | Yes      | VAT rate as a **percentage**. Example: `21` for 21%              |
| `client_country` | string  | Yes      | 2-letter ISO country code of the client requesting validation   |
| `vat_number`     | string  | No       | VAT number to validate via VIES (can include country prefix, e.g., "DE12345678" or just "12345678") |
| `vat_country`    | string  | No       | 2-letter ISO country code (required if `vat_number` is provided and doesn't have a prefix) |

---

## Connection Check (GET `https://valora.spotahome.com/api/v1/open/vat-vies/calculate`)

If no parameters are sent, the endpoint responds with the expected payload structure:

```json
{
  "message": "You are connected!",
  "expected_payload": {
    "vat_amount": "integer, required (in cents, e.g., 2999 for €29.99)",
    "vat_rate": "integer, required (percentage, e.g., 21 for 21%)",
    "client_country": "string, required (2-letter ISO code)",
    "vat_number": "string, nullable",
    "vat_country": "string, nullable (2-letter ISO code)"
  }
}
```

If parameters are sent via GET, the endpoint responds with a **400 Bad Request**:

```json
{
  "error": true,
  "message": "GET method not supported for calculations. Perhaps you meant to use POST"
}
```

---

## Responses

### VIES Status Check - Success (200)

```json
{
  "success": true,
  "vat_validation": {
    "is_valid": true,
    "checked": true,
    "skipped": false,
    "server_down": false,
    "vat_number": "12345678",
    "vat_country": "FR",
    "client_country": "ES",
    "trader_name": "Example Company Ltd",
    "trader_address": "123 Street, City, Country"
  }
}
```

**Note:** The `vat_number` and `vat_country` in the response represent the **validated/parsed values** used for the VIES check. If the input VAT number included a country prefix (e.g., "DE12345678"), the system will automatically parse it and return the cleaned number ("12345678") and extracted country code ("DE") in the response, regardless of the `vat_country` parameter provided.

### VAT Calculation - Success (200)

```json
{
  "success": true,
  "vat_validation": {
    "is_valid": true,
    "checked": true,
    "skipped": false,
    "vat_number": "12345678",
    "vat_country": "FR",
    "client_country": "ES"
  },
  "amounts": {
    "net": 2999,
    "gross": 2999,
    "vat_applied": 0,
    "vat_rate_applied": 0
  }
}
```

**Notes:**
* When a valid intra-community VAT number is validated, `vat_rate_applied` will be `0` (VAT exemption applies).
* The `vat_number` and `vat_country` in the response represent the **validated/parsed values** used for the VIES check. If the input VAT number included a country prefix, the system will automatically parse it and return the cleaned values.

### Validation Error (422)

```json
{
  "error": true,
  "message": "Validation failed",
  "errors": {
    "vat_amount": ["The vat_amount field is required."],
    "vat_rate": ["The vat_rate field must be between 0 and 100."]
  }
}
```

### VIES Unavailable (503)

```json
{
  "error": true,
  "message": "VIES service is temporarily unavailable. Please try again later."
}
```

### Unexpected Error (500)

```json
{
  "error": true,
  "message": "An unexpected error occurred while processing your request."
}
```

### GET with Parameters (400) - Calculate Endpoint

```json
{
  "error": true,
  "message": "GET method not supported for calculations. Perhaps you meant to use POST"
}
```

### POST to Status Endpoint (400)

```json
{
  "error": true,
  "message": "POST method not supported for calculations. Perhaps you meant to use GET"
}
```

### POST with Missing Parameters (422)

```json
{
  "error": true,
  "message": "Please provide valid parameters.",
  "expected_payload": {
    "vat_amount": "integer, required (in cents, e.g., 2999 for €29.99)",
    "vat_rate": "integer, required (percentage, e.g., 21 for 21%)",
    "client_country": "string, required (2-letter ISO code)",
    "vat_number": "string, nullable",
    "vat_country": "string, nullable (2-letter ISO code)"
  }
}
```

---

## Important Notes

### General
* VAT amounts are expected in **cents** for precision.
* VAT rates must be between `0` and `100`.
* All country codes must be 2-letter ISO codes (e.g., `ES`, `FR`, `DE`).

### VIES Validation
* VIES checks are only performed if both `vat_number` and `vat_country` are provided (or if `vat_number` includes a country prefix).
* **VAT Number Parsing:** The system automatically parses VAT numbers. If a VAT number includes a country prefix (e.g., "DE12345678"), the prefix is extracted and used as the country code, and the remaining digits are used as the VAT number. If no prefix is found, the provided `vat_country` parameter is used.
* **Same-country restriction:** If the validated `vat_country` (after parsing) matches `client_country`, validation will be skipped (intra-community VAT validation only works between different countries).
* The system uses automatic caching (5-day cache) to improve performance and reduce API calls to the VIES service.
* If the VAT number is validated successfully and is from a different EU country, the intra-community VAT exemption applies, setting VAT to `0%`.
* **Response Values:** The `vat_number` and `vat_country` fields in the response always reflect the **validated/parsed values** that were actually used for the VIES check, not necessarily the input values.

### Endpoint Usage
* Use `GET https://valora.spotahome.com/api/v1/open/vat-vies/status` for VAT validation only (no calculations).
* Use `POST https://valora.spotahome.com/api/v1/open/vat-vies/calculate` for VAT calculations with optional validation.
* Use `GET https://valora.spotahome.com/api/v1/open/vat-vies/calculate` (no parameters) to check connection and see expected payload.

---

© Valora API Documentation


---
